@sema-agent/settings-schema 15.1.0 → 16.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/README.md +60 -46
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c16-deep-nesting-205/expected.json +2 -2
  5. package/conformance/cases/c16-deep-nesting-205/meta.json +1 -1
  6. package/conformance/cases/c50-s784-lane-host-legal/expected.json +6 -2
  7. package/conformance/cases/c50-s784-lane-host-legal/input.json +3 -1
  8. package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +6 -2
  9. package/conformance/cases/c51-s784-lane-e2b-legal/input.json +3 -1
  10. package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +6 -2
  11. package/conformance/cases/c52-s784-lane-k8s-legal/input.json +3 -1
  12. package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +6 -2
  13. package/conformance/cases/c53-s784-lane-ssh-legal/input.json +3 -1
  14. package/conformance/cases/c54-s784-lane-adb-legal/expected.json +6 -2
  15. package/conformance/cases/c54-s784-lane-adb-legal/input.json +3 -1
  16. package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +6 -2
  17. package/conformance/cases/c55-s784-lane-local-docker-legal/input.json +3 -1
  18. package/conformance/cases/c56-s784-lane-device-legal/expected.json +8 -2
  19. package/conformance/cases/c56-s784-lane-device-legal/input.json +5 -2
  20. package/conformance/cases/c57-s784-g1-enabled-no-lane/meta.json +1 -1
  21. package/conformance/cases/c58-s784-g2-multitenant-host/input.json +3 -1
  22. package/conformance/cases/c59-s784-g3-k8s-missing-image/input.json +3 -1
  23. package/conformance/cases/c60-s784-g3-e2b-missing-key/input.json +3 -1
  24. package/conformance/cases/c61-s784-g3-other-lane-key/input.json +3 -1
  25. package/conformance/cases/c62-s784-g7a-session-tidb/input.json +3 -1
  26. package/conformance/cases/c63-s784-host-two-replicas/input.json +3 -1
  27. package/conformance/cases/c64-s784-multitenant-local-store/input.json +3 -1
  28. package/conformance/cases/c65-s784-pg-without-db/input.json +3 -1
  29. package/conformance/cases/c66-s784-memory-backend-mismatch/input.json +3 -1
  30. package/conformance/cases/c66-s784-memory-backend-mismatch/meta.json +1 -1
  31. package/conformance/cases/c68-s784-device-local-store/input.json +6 -1
  32. package/conformance/cases/cli04-env-value-with-newline/expected.json +6 -2
  33. package/conformance/cases/cli04-env-value-with-newline/input.json +3 -1
  34. package/dist/cli.d.ts +1 -1
  35. package/dist/config-d-file.d.ts +41 -5
  36. package/dist/config-d-file.js +105 -22
  37. package/dist/config-document-refusal.d.ts +39 -0
  38. package/dist/config-document-refusal.js +2 -0
  39. package/dist/device-relay.d.ts +21 -0
  40. package/dist/device-relay.js +23 -0
  41. package/dist/env-model-table.d.ts +2 -9
  42. package/dist/env-model-table.js +6 -14
  43. package/dist/execution-lane-index.d.ts +21 -17
  44. package/dist/execution-lane-index.js +28 -20
  45. package/dist/execution-lanes.d.ts +20 -5
  46. package/dist/execution-lanes.js +31 -7
  47. package/dist/file-store.d.ts +30 -3
  48. package/dist/file-store.js +89 -32
  49. package/dist/fleet.d.ts +6 -8
  50. package/dist/fleet.js +5 -7
  51. package/dist/hooks.d.ts +1 -1
  52. package/dist/index.d.ts +2 -1
  53. package/dist/index.js +4 -3
  54. package/dist/migration-receipt.d.ts +1 -1
  55. package/dist/migration-receipt.js +1 -1
  56. package/dist/one-time-migrations.d.ts +1 -1
  57. package/dist/one-time-migrations.js +166 -45
  58. package/dist/remote-exec.js +11 -4
  59. package/dist/safety-merge-spec.d.ts +1 -1
  60. package/dist/safety-merge-spec.js +1 -1
  61. package/dist/types.d.ts +15 -11
  62. package/dist/types.js +100 -49
  63. package/dist/url-shape.d.ts +18 -0
  64. package/dist/url-shape.js +22 -0
  65. package/docs/API-SURFACE.md +18 -7
  66. package/docs/CONFORMANCE.md +1 -7
  67. package/docs/CONSUMER-INTEGRATION.md +4 -4
  68. package/llms-full.txt +27 -25
  69. package/llms.txt +10 -8
  70. package/package.json +1 -1
@@ -1,5 +1,4 @@
1
1
  import { type EnvModelsDecision } from "./config-root-documents.js";
2
- import { SUMMARIZE_MODEL_MISSING_CODE } from "./fleet.js";
3
2
  import type { ModelRole } from "./types.js";
4
3
  /** 一份 env 快照(`process.env` 也满足这个形)。 */
5
4
  export type EnvSnapshot = Readonly<Record<string, string | undefined>>;
@@ -142,12 +141,6 @@ export type EnvModelTableWritePlan = {
142
141
  readonly receipt: EnvModelTableReceipt;
143
142
  readonly confirmKey: string;
144
143
  readonly intent: "absent" | "stale";
145
- }
146
- /** 不可确认:先改文档(summarize 要落到一条启用条目)。 */
147
- | {
148
- readonly kind: "fix_first";
149
- readonly code: typeof SUMMARIZE_MODEL_MISSING_CODE;
150
- readonly receipt: EnvModelTableReceipt;
151
144
  };
152
145
  /**
153
146
  * 写者在写 `config.d/models.json` 之前调的**计划**(判序见文件头)。纯函数:不读环境、不读盘、不改入参。
@@ -155,9 +148,9 @@ export type EnvModelTableWritePlan = {
155
148
  */
156
149
  export declare function planEnvModelTableWrite(input: EnvModelTableWriteInput): EnvModelTableWritePlan;
157
150
  /**
158
- * 计划停下来的那两种结局(`confirm` / `fix_first`)的**一句话**(三端同一句;只用回执里的 env 键名、目录名与模型 id —— 可披露的原样,秘密形的打成记号;凭据与别的 env 值从不进句子)。
151
+ * 计划停下来的那一种结局(`confirm`)的**一句话**(三端同一句;只用回执里的 env 键名、目录名与模型 id —— 可披露的原样,秘密形的打成记号;凭据与别的 env 值从不进句子)。
159
152
  * 写类结局不是拒 ⇒ `TypeError`。
160
153
  */
161
154
  export declare function envModelTableWriteRefusal(plan: Extract<EnvModelTableWritePlan, {
162
- readonly kind: "confirm" | "fix_first";
155
+ readonly kind: "confirm";
163
156
  }>): string;
@@ -7,21 +7,21 @@
7
7
  * cheap 槽四态({@link EnvCheapSlot})。「env 模型表已声明」({@link envModelTableDeclared})与角色底({@link buildEnvModelBaseline})都由它派生 ——
8
8
  * 这道闸的执行方(引擎一侧;今天引擎还不强制)与写者的计划助手调同一份(一次性迁移 SS-M11 只用两槽点名被顶替的 env 模型,铸不铸不看它),不各写一份。env 快照按「读到即算」(下标读;原型分层的快照里继承来的键算在场)。
9
9
  * 引擎另有「名册占位 ⇒ env 表算空」一格,那是引擎自己的部署事实,不在本包。
10
- * · **计划助手判序**({@link planEnvModelTableWrite}):目标腿不读 `config.d`(remote / none)⇒ 照写 → 候选不管模型面 ⇒ 照写 → summarize 会悬空 ⇒
11
- * `fix_first`(不可确认:每个任务都要解 summarize,采纳后任务全拒)→ env 表未声明 ⇒ 照写、**不写确认** → 配置根的决定是 `replaced` ⇒ 照写 →
10
+ * · **计划助手判序**({@link planEnvModelTableWrite}):目标腿不读 `config.d`(remote / none)⇒ 照写 → 候选不管模型面 ⇒ 照写 →
11
+ * env 表未声明 ⇒ 照写、**不写确认** → 配置根的决定是 `replaced` ⇒ 照写 →
12
12
  * 调用方带回的确认键等于这一次的 ⇒ `write_with_decision`(先经 `writeEnvModelsDecision` 写 `replaced`,再写目录;两次写在写者对 models.json 的同一把锁里)→
13
13
  * 否则 `confirm`(把回执给用户看)。执法在引擎一侧(本地腿的顶替闸;今天引擎不拒这个组),本助手只是让写者在写之前知道这道闸强制起来会不会拒、会顶掉什么。
14
14
  * · **回执按 id**:每个退场的 env 条目带 `servedAs`(候选里同 id 的启用条目名 —— 上游模型仍被服务,形可能不同);每个 env 底角色带写前绑谁
15
15
  * (env 表键 + 它解到的 id)与采纳后落到哪个条目(`{name, id}`)。缺省落点经 `resolveCatalogDefaultModel`(与引擎的 stage 同一只解析器);
16
16
  * 绑到停用条目或目录外的名 = `dangles`。确认键 = 回执的定序 JSON —— 后果(落点名 / id、env 槽、决定读数)一变,旧键就过期。
17
- * · 可用性(引擎的 brain 腿、端点、读腿逐条丢)计划判不了;决定性的 summarize 那一格由引擎的不变量兜。
17
+ * · 可用性(引擎的 brain 腿、端点、读腿逐条丢)计划判不了。悬空的 summarize 不是拒(16.0.0 起;此前计划把它判成「不可确认」,前提不成立,那一臂删了):引擎把点名目录外 / 停用条目的角色席
18
+ * 读成未设、按角色回退链落下一级并说一次 —— 没有哪种读法因它拒组;它照样写在回执里(`dangles`),要确认的写法把它给用户看。
18
19
  */
19
20
  import { z } from "zod";
20
21
  import { entryEnabledFlag, catalogManagesModelPlane } from "./catalog-manages.js";
21
22
  import { parseDomainSalvaged, isDisclosableIdentity, resolveActiveTiers, withheldNameToken } from "./config-fns.js";
22
23
  import { ENV_MODELS_DOCUMENT } from "./config-root-documents.js";
23
24
  import { resolveCatalogDefaultModel } from "./default-model.js";
24
- import { SUMMARIZE_MODEL_MISSING_CODE } from "./fleet.js";
25
25
  import { landCatalogWord } from "./tier-words.js";
26
26
  import { stableStringify } from "./hash.js";
27
27
  import { captureJson, dataDescriptor, isolatedObject } from "./own-key-read.js";
@@ -205,8 +205,6 @@ export function planEnvModelTableWrite(input) {
205
205
  return { kind: "write", why: "not_managing" };
206
206
  const env = snapshotEnvModelTable(input.env); // read once: the receipt and the env-table rule answer from the same reading
207
207
  const receipt = buildReceipt(buildEnvModelBaseline(env), candidateView(copy.value), decision);
208
- if (receipt.roles.find((r) => r.role === "summarize")?.after.kind === "dangles")
209
- return { kind: "fix_first", code: SUMMARIZE_MODEL_MISSING_CODE, receipt };
210
208
  if (!envModelTableDeclared(env))
211
209
  return { kind: "write", why: "env_table_empty" };
212
210
  if (decision === "replaced")
@@ -226,7 +224,7 @@ const landingText = (after) => {
226
224
  return after.kind === "dangles" ? "nothing (no enabled catalog entry)" : "an entry the engine selects ({select})";
227
225
  };
228
226
  /**
229
- * 计划停下来的那两种结局(`confirm` / `fix_first`)的**一句话**(三端同一句;只用回执里的 env 键名、目录名与模型 id —— 可披露的原样,秘密形的打成记号;凭据与别的 env 值从不进句子)。
227
+ * 计划停下来的那一种结局(`confirm`)的**一句话**(三端同一句;只用回执里的 env 键名、目录名与模型 id —— 可披露的原样,秘密形的打成记号;凭据与别的 env 值从不进句子)。
230
228
  * 写类结局不是拒 ⇒ `TypeError`。
231
229
  */
232
230
  export function envModelTableWriteRefusal(plan) {
@@ -245,13 +243,7 @@ export function envModelTableWriteRefusal(plan) {
245
243
  return (`writing this models document replaces the env model table on the local config leg: ${models}; after adoption ${roles}.${decision} ` +
246
244
  `To let the catalog replace them, confirm — the writer records ${file} = {"envModels":"replaced"} in the config root first, then writes the models document; to keep the env models, do not write it.`);
247
245
  }
248
- case "fix_first": {
249
- const row = plan.receipt.roles.find((x) => x.role === "summarize");
250
- const ref = row === undefined ? "" : ` (env binding ${shown(row.before.ref)})`;
251
- return (`this models document leaves the summarize role unresolved${ref}: it lands on no enabled catalog entry, so every task would be refused after adoption (${SUMMARIZE_MODEL_MISSING_CODE}). ` +
252
- "Bind roles.summarize to an enabled entry of the document, or add an enabled entry under the name it points at, then plan the write again.");
253
- }
254
246
  default:
255
- throw new TypeError(`envModelTableWriteRefusal: only a "confirm" or "fix_first" plan is a refusal (got ${JSON.stringify(kind)})`);
247
+ throw new TypeError(`envModelTableWriteRefusal: only a "confirm" plan is a refusal (got ${JSON.stringify(kind)})`);
256
248
  }
257
249
  }
@@ -1,18 +1,20 @@
1
1
  /**
2
2
  * 车道表({@link EXECUTION_LANES})的两只**派生索引** + 「迁移能搬哪些键」的**一份**键集,
3
3
  * 供 `types.ts`(写闸)与 `one-time-migrations.ts`(一次性迁移 SS-M4)同读。
4
- * 本模块是包内私有的:不进 `package.json` 的 `exports`(与 `secret-scan.ts` 同一姿势),主桶也只具名取走一个名字 ——
5
- * 必需坐标算不算「缺」的唯一判官 {@link isBlankCoordinate}(消费方要在自己一侧问同一个问题,调包根上这一只,不抄第二份);
6
- * 其余导出(索引、键集、谓词)仍不进主桶 —— 消费方只拿表本身,它们是本包的实现细节。
4
+ * 本模块是包内私有的:不进 `package.json` 的 `exports`(与 `secret-scan.ts` 同一姿势),主桶也不取它的任何名字 —— 索引、键集、谓词是本包的实现细节,消费方只拿表本身。
5
+ * (必需坐标算不算「缺」的唯一判官 `isBlankCoordinate` 不在这里:它住在零依赖的叶模块 `./execution-lanes`,包根与 `./execution-lanes` 子路径是同一只。)
7
6
  * 索引只从表派生,不另存任何词:表改了,这里跟着变。
8
7
  *
9
- * ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts`)
10
- * 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)不带**。
11
- * 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS}),写闸读的是**同一只谓词**
12
- * ({@link isMigratableEnvKey}),不是第二张手抄的表 —— 迁移多认一个键而写闸没跟,或反过来,不会静默分叉
8
+ * ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts`;16.0.0 起指路点名**哪一条**迁移)
9
+ * 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)不带**;带的话点名搬它的那一条迁移。
10
+ * 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS}),写闸读的是**同一只函数**
11
+ * ({@link envKeyMigration}),不是第二张手抄的表 —— 迁移多认一个键而写闸没跟,或反过来,不会静默分叉
13
12
  * (`test/migration-signpost-11.0.0.test.ts` 用真跑迁移的读数逐个保留名对拍)。
13
+ * 哪一条:车道表登记 7.0.0 之后才进表的车道键({@link LATE_LANE_ENV_KEYS})在 7.0.0 起的存量行里可以**合法地**写在 `env` / `secretEnv` 逃生口里
14
+ * (那时它还不是保留名),它的旧形归登记它那一版的迁移;其余键的旧形只可能出自 6.x 行,归 SS-M4。
14
15
  */
15
16
  import { type ExecutionLane, type ExecutionLaneRow } from "./execution-lanes.js";
17
+ import type { OneTimeMigrationId } from "./migration-receipt.js";
16
18
  /** 一条车道的表行。词表与表行由 `execution-lanes.ts` 的 `satisfies Record<ExecutionLane, …>` 一一对应 ——
17
19
  * 找不到 = 本包自身的不变量坏了(不是输入错),所以抛。 */
18
20
  export declare function laneRowOf(lane: ExecutionLane): ExecutionLaneRow;
@@ -24,20 +26,22 @@ export interface LaneEnvOwner {
24
26
  }
25
27
  /** 车道 env 名 → 归属。四栏两两不交、跨车道不交(`test/execution-lanes-7.0.0.test.ts` 钉),所以每个名恰好一个归属。 */
26
28
  export declare const LANE_ENV_OWNER: ReadonlyMap<string, LaneEnvOwner>;
27
- /**
28
- * 车道坐标的**唯一判官**:一个必需坐标「未设 / 空串 / 只含空白」一律算缺。本函数判其中的后两种,且只看**已经有的字符串值**:空串,或只含空白(`String.prototype.trim` 认的空白:空格 / 制表 / 换行 / 不换行空格 / 字节序标记 …)⇒ `true`;
29
- * 「这一键根本没设」由调用方先判(写闸对没设的键另有一句 `requires coords.<键>`,两种缺法同样被拒)。
30
- * 写闸对所有车道、所有必需坐标同判这一只;它只判「是不是空白」,不 trim 值、不管值的格式(格式不在这一条的范围:`" sandboxes "` 不是空白)。
31
- * 它只答「这个值是不是空白」,不管这个键是必需还是可选:据它**拒不拒**一个坐标,由调用方按键的必需 / 可选决定 —— 写闸只对必需坐标据它拒;个别可选坐标上空串有语义(如 `K8S_RUNTIME_CLASS` 的空串 = 不用 Kata),可选坐标的空串照收。
32
- */
33
- export declare function isBlankCoordinate(value: string): boolean;
34
29
  /** 一个车道 env 名在 WorkerSpec 里的**唯一写位**(保留键的拒因指路用它)。 */
35
30
  export declare function laneEnvSeat(name: string, owner: LaneEnvOwner): string;
36
- /** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.lane`、`REQUIRE_EXECUTION_LANE` 丢弃)。 */
31
+ /** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.lanes`、`REQUIRE_EXECUTION_LANE` 丢弃)。 */
37
32
  export declare const LANE_WORD_KEYS: readonly ["REMOTE_EXEC", "REQUIRE_EXECUTION_LANE"];
38
33
  /** 渲染器以前不渲、运维只能写在 env 逃生口里的店键(7.0.0 起归 `runtime.store`;迁移搬进 `store`)。 */
39
34
  export declare const STORE_ENV_KEYS: readonly ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BACKEND"];
40
35
  /** 一个名字是不是车道块的键:车道词两键,或车道表里的任一车道 env 名。 */
41
36
  export declare function isLaneEnvKey(name: string): boolean;
42
- /** 一个名字是不是迁移会处理的 6.x env 键(搬进结构块 / 丢弃 / 点名它拒):车道块的键 ∪ 店键。写闸拒因带不带迁移指路只看它。 */
43
- export declare function isMigratableEnvKey(name: string): boolean;
37
+ /** 搬一个逃生口里的保留名的那一条一次性迁移,与它改写的存量形是哪一版之前存下的(指路句 `migrationSignpost(migration, before)` 的两个参数)。 */
38
+ export interface EnvKeyMigration {
39
+ readonly migration: OneTimeMigrationId;
40
+ readonly before: string;
41
+ }
42
+ /** 7.0.0 之后才登记进车道表的车道 env 名 → 改写它的迁移(只增不减;每加一个,在这里写一行)。16.0.0:`DEVICE_RELAY_URL`(device 行的必需坐标;
43
+ * 此前 device 臂就读它而表漏登记,7.0.0–15.x 的写闸把它当普通 env 收下)→ SS-M12。 */
44
+ export declare const LATE_LANE_ENV_KEYS: ReadonlyMap<string, EnvKeyMigration>;
45
+ /** 一个 env 名在逃生口里 ⇒ 哪一条迁移搬它(见文件头「迁移指路」):晚登记的车道键 ⇒ 登记它那一版的迁移;别的车道块键与店键 ⇒ SS-M4;其余 ⇒ `undefined`(没有迁移搬它)。
46
+ * 写闸拒因带不带指路、指哪一条只看它。 */
47
+ export declare function envKeyMigration(name: string): EnvKeyMigration | undefined;
@@ -1,16 +1,17 @@
1
1
  /**
2
2
  * 车道表({@link EXECUTION_LANES})的两只**派生索引** + 「迁移能搬哪些键」的**一份**键集,
3
3
  * 供 `types.ts`(写闸)与 `one-time-migrations.ts`(一次性迁移 SS-M4)同读。
4
- * 本模块是包内私有的:不进 `package.json` 的 `exports`(与 `secret-scan.ts` 同一姿势),主桶也只具名取走一个名字 ——
5
- * 必需坐标算不算「缺」的唯一判官 {@link isBlankCoordinate}(消费方要在自己一侧问同一个问题,调包根上这一只,不抄第二份);
6
- * 其余导出(索引、键集、谓词)仍不进主桶 —— 消费方只拿表本身,它们是本包的实现细节。
4
+ * 本模块是包内私有的:不进 `package.json` 的 `exports`(与 `secret-scan.ts` 同一姿势),主桶也不取它的任何名字 —— 索引、键集、谓词是本包的实现细节,消费方只拿表本身。
5
+ * (必需坐标算不算「缺」的唯一判官 `isBlankCoordinate` 不在这里:它住在零依赖的叶模块 `./execution-lanes`,包根与 `./execution-lanes` 子路径是同一只。)
7
6
  * 索引只从表派生,不另存任何词:表改了,这里跟着变。
8
7
  *
9
- * ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts`)
10
- * 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)不带**。
11
- * 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS}),写闸读的是**同一只谓词**
12
- * ({@link isMigratableEnvKey}),不是第二张手抄的表 —— 迁移多认一个键而写闸没跟,或反过来,不会静默分叉
8
+ * ## 迁移指路(7.0.1;11.0.0 起指路句住 `retired-form.ts`;16.0.0 起指路点名**哪一条**迁移)
9
+ * 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)不带**;带的话点名搬它的那一条迁移。
10
+ * 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS}),写闸读的是**同一只函数**
11
+ * ({@link envKeyMigration}),不是第二张手抄的表 —— 迁移多认一个键而写闸没跟,或反过来,不会静默分叉
13
12
  * (`test/migration-signpost-11.0.0.test.ts` 用真跑迁移的读数逐个保留名对拍)。
13
+ * 哪一条:车道表登记 7.0.0 之后才进表的车道键({@link LATE_LANE_ENV_KEYS})在 7.0.0 起的存量行里可以**合法地**写在 `env` / `secretEnv` 逃生口里
14
+ * (那时它还不是保留名),它的旧形归登记它那一版的迁移;其余键的旧形只可能出自 6.x 行,归 SS-M4。
14
15
  */
15
16
  import { EXECUTION_LANES } from "./execution-lanes.js";
16
17
  /** 一条车道的表行。词表与表行由 `execution-lanes.ts` 的 `satisfies Record<ExecutionLane, …>` 一一对应 ——
@@ -26,20 +27,11 @@ export const LANE_ENV_OWNER = new Map(EXECUTION_LANES.flatMap((r) => {
26
27
  const own = (names, secret, required) => names.map((k) => [k, { lane: r.lane, secret, required }]);
27
28
  return [...own(r.requiredEnv, false, true), ...own(r.optionalEnv, false, false), ...own(r.requiredSecretEnv, true, true), ...own(r.optionalSecretEnv, true, false)];
28
29
  }));
29
- /**
30
- * 车道坐标的**唯一判官**:一个必需坐标「未设 / 空串 / 只含空白」一律算缺。本函数判其中的后两种,且只看**已经有的字符串值**:空串,或只含空白(`String.prototype.trim` 认的空白:空格 / 制表 / 换行 / 不换行空格 / 字节序标记 …)⇒ `true`;
31
- * 「这一键根本没设」由调用方先判(写闸对没设的键另有一句 `requires coords.<键>`,两种缺法同样被拒)。
32
- * 写闸对所有车道、所有必需坐标同判这一只;它只判「是不是空白」,不 trim 值、不管值的格式(格式不在这一条的范围:`" sandboxes "` 不是空白)。
33
- * 它只答「这个值是不是空白」,不管这个键是必需还是可选:据它**拒不拒**一个坐标,由调用方按键的必需 / 可选决定 —— 写闸只对必需坐标据它拒;个别可选坐标上空串有语义(如 `K8S_RUNTIME_CLASS` 的空串 = 不用 Kata),可选坐标的空串照收。
34
- */
35
- export function isBlankCoordinate(value) {
36
- return value.trim() === "";
37
- }
38
30
  /** 一个车道 env 名在 WorkerSpec 里的**唯一写位**(保留键的拒因指路用它)。 */
39
31
  export function laneEnvSeat(name, owner) {
40
32
  return `runtime.execution.${owner.secret ? "secretRefs" : "coords"}.${name}`;
41
33
  }
42
- /** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.lane`、`REQUIRE_EXECUTION_LANE` 丢弃)。 */
34
+ /** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.lanes`、`REQUIRE_EXECUTION_LANE` 丢弃)。 */
43
35
  export const LANE_WORD_KEYS = ["REMOTE_EXEC", "REQUIRE_EXECUTION_LANE"];
44
36
  /** 渲染器以前不渲、运维只能写在 env 逃生口里的店键(7.0.0 起归 `runtime.store`;迁移搬进 `store`)。 */
45
37
  export const STORE_ENV_KEYS = ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BACKEND"];
@@ -47,7 +39,23 @@ export const STORE_ENV_KEYS = ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BAC
47
39
  export function isLaneEnvKey(name) {
48
40
  return LANE_WORD_KEYS.some((k) => k === name) || LANE_ENV_OWNER.has(name);
49
41
  }
50
- /** 一个名字是不是迁移会处理的 6.x env 键(搬进结构块 / 丢弃 / 点名它拒):车道块的键 ∪ 店键。写闸拒因带不带迁移指路只看它。 */
51
- export function isMigratableEnvKey(name) {
52
- return isLaneEnvKey(name) || STORE_ENV_KEYS.some((k) => k === name);
42
+ const SIX_X_ENV = Object.freeze({ migration: "SS-M4", before: "7.0.0" });
43
+ /** 7.0.0 之后才登记进车道表的车道 env 名 → 改写它的迁移(只增不减;每加一个,在这里写一行)。16.0.0:`DEVICE_RELAY_URL`(device 行的必需坐标;
44
+ * 此前 device 臂就读它而表漏登记,7.0.0–15.x 的写闸把它当普通 env 收下)→ SS-M12。 */
45
+ export const LATE_LANE_ENV_KEYS = lateLaneEnvKeys([["DEVICE_RELAY_URL", Object.freeze({ migration: "SS-M12", before: "16.0.0" })]]);
46
+ /** 建 {@link LATE_LANE_ENV_KEYS}:每一行的名字必须在车道表里(载入时验,名字拼错 ⇒ 包载入即抛,不静默成一条永不命中的行)。 */
47
+ function lateLaneEnvKeys(rows) {
48
+ for (const [name] of rows) {
49
+ if (!LANE_ENV_OWNER.has(name))
50
+ throw new Error(`LATE_LANE_ENV_KEYS names "${name}", which is not in the execution lane table (package invariant broken)`);
51
+ }
52
+ return new Map(rows);
53
+ }
54
+ /** 一个 env 名在逃生口里 ⇒ 哪一条迁移搬它(见文件头「迁移指路」):晚登记的车道键 ⇒ 登记它那一版的迁移;别的车道块键与店键 ⇒ SS-M4;其余 ⇒ `undefined`(没有迁移搬它)。
55
+ * 写闸拒因带不带指路、指哪一条只看它。 */
56
+ export function envKeyMigration(name) {
57
+ const late = LATE_LANE_ENV_KEYS.get(name);
58
+ if (late !== undefined)
59
+ return late;
60
+ return isLaneEnvKey(name) || STORE_ENV_KEYS.some((k) => k === name) ? SIX_X_ENV : undefined;
53
61
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 执行车道 + 存储后端的**词表正本**(7.0.0,M-R2 WorkerSpec 结构块)。纯数据常量,零函数、零依赖(叶模块:
3
- * 本文件不 import 任何东西,`types.ts` 反过来引它)。
2
+ * 执行车道 + 存储后端的**词表正本**(7.0.0,M-R2 WorkerSpec 结构块)。纯数据常量,外加两只纯判官函数(退役车道词的拒句 `retiredLaneWordRefusal`、
3
+ * 车道坐标的空白判官 `isBlankCoordinate`),零依赖(叶模块:本文件不 import 任何东西,`types.ts` 反过来引它)。
4
4
  *
5
5
  * ## 为什么在这里
6
6
  * 车道词、每条车道读哪些 env、哪条车道分租户、存储后端词 —— 这几张表以前在四处各抄一份(引擎的能力表、
@@ -17,6 +17,8 @@
17
17
  * · `tenantSeparation`:同文件 `executionLaneCaps` 每臂的 `tenantSeparation` 位(:209 host / :230 e2b / :249 k8s /
18
18
  * :269 ssh / :288 adb / :308 local-docker / :327 device)。`tenantSeparationBasis`(11.0.0):分租户的那几臂凭什么分 ——
19
19
  * e2b / k8s / local-docker 每任务一只隔离目标(`isolation`),device 是发起者自己的设备(`principal-binding`)。
20
+ * · `singleReplica`(16.0.0):这条车道的执行目标挂在**一个副本**上 —— host 是副本自己的文件系统,device 是设备连上的那一个副本
21
+ * (设备的连接落在一个副本上,跨副本把会话送到它的设备今天没有)。表里有一条这种车道的 worker 只能一个副本(写闸按这一列判,不点名车道词)。
20
22
  * · `requiredEnv ∪ requiredSecretEnv`:这条车道缺了就起不了服的键。最初逐字抄自 `src/config.ts` 的手写表 `REQUIRED_ENVS_BY_LANE`(:1054),
21
23
  * 那张手写表已退役 —— 服务端的必需 env 门按本表行的这两栏拒启,部署面的车道清单读同一行,所以今天**本表就是**必需键的唯一来源。按 `src/config-catalog.ts`
22
24
  * 该键那一行的值类型分栏 —— 类型 `secret` / `opaque`(目录自己「绝不回显值」的两类)进 `*SecretEnv`,其余进 `*Env`。
@@ -66,9 +68,11 @@ export type LaneTenancy = {
66
68
  /** 一条车道的表行。四栏 env 名两两不交、跨车道也不交(`test/execution-lanes-7.0.0.test.ts` 钉)。
67
69
  * - `requiredEnv` / `optionalEnv`:非密坐标 —— 写在 `runtime.execution.coords`(字面值);
68
70
  * - `requiredSecretEnv` / `optionalSecretEnv`:密钥 —— 写在 `runtime.execution.secretRefs`(服务变量名 → 宿主 env-NAME,绝不写值);
69
- * - 租户分离位 + 依据:见 {@link LaneTenancy}。 */
71
+ * - 租户分离位 + 依据:见 {@link LaneTenancy}。
72
+ * - `singleReplica`(16.0.0):执行目标挂在一个副本上 ⇒ 声明了这条车道的 worker 只能一个副本(见文件头)。 */
70
73
  export type ExecutionLaneRow = {
71
74
  readonly lane: ExecutionLane;
75
+ readonly singleReplica: boolean;
72
76
  readonly requiredEnv: readonly string[];
73
77
  readonly requiredSecretEnv: readonly string[];
74
78
  readonly optionalEnv: readonly string[];
@@ -76,6 +80,16 @@ export type ExecutionLaneRow = {
76
80
  } & LaneTenancy;
77
81
  /** 车道表(次序 = {@link EXECUTION_LANE_WORDS})。整张表深冻结 —— 它是唯一正本,消费方拿到的不许被改。 */
78
82
  export declare const EXECUTION_LANES: readonly ExecutionLaneRow[];
83
+ /**
84
+ * 车道坐标的**唯一判官**:一个必需坐标「未设 / 空串 / 只含空白」一律算缺。本函数判其中的后两种,且只看**已经有的字符串值**:空串,或只含空白(`String.prototype.trim` 认的空白:空格 / 制表 / 换行 / 不换行空格 / 字节序标记 …)⇒ `true`;
85
+ * 「这一键根本没设」由调用方先判(写闸对没设的键另有一句 `requires coords.<键>`,两种缺法同样被拒)。
86
+ * 写闸对所有车道、所有必需坐标同判这一只;本地文件契约 `RemoteExecSpec`(`config.d/remote-exec.json`)的每个非空串字段(镜像 / 命名空间 / 主机 / 路径 / …)问的也是这一只(16.0.0 起:同一个部署声明在两处说,「空白」不再有两种读法);它只判「是不是空白」,不 trim 值、不管值的格式(格式不在这一条的范围:`" sandboxes "` 不是空白)。
87
+ * 它只答「这个值是不是空白」,不管这个键是必需还是可选:据它**拒不拒**一个坐标,由调用方按键的必需 / 可选决定 —— 写闸只对必需坐标据它拒;个别可选坐标上空串有语义(如 `K8S_RUNTIME_CLASS` 的空串 = 不用 Kata),可选坐标的空串照收。
88
+ *
89
+ * 住在这个零依赖的叶模块里:包根(`export *`)与 `./execution-lanes` 子路径是**同一个函数对象** —— 只想问这一个问题的消费方走子路径,不必冷载入整只契约包桶;
90
+ * 车道表的派生索引与写闸也从这里 import,全包只有这一处函数体。
91
+ */
92
+ export declare function isBlankCoordinate(value: string): boolean;
79
93
  /** 存储后端(→ `DB_BACKEND`)。= server `src/config-types.ts` `DB_BACKEND_WORDS`(:67)逐字同序。 */
80
94
  export declare const STORE_BACKENDS: readonly ["mysql", "pg", "local", "memory"];
81
95
  export type StoreBackend = (typeof STORE_BACKENDS)[number];
@@ -83,6 +97,7 @@ export type StoreBackend = (typeof STORE_BACKENDS)[number];
83
97
  * (`tidb` 不在集内:引擎见之拒启)。 */
84
98
  export declare const SESSION_BACKENDS: readonly ["memory", "mysql", "auto"];
85
99
  export type SessionBackend = (typeof SESSION_BACKENDS)[number];
86
- /** 记忆持久面方言(→ `MEMORY_ENGINE_BACKEND`)。= server `src/config.ts` :3436 闭集 / 目录 :468 `enumValues` 逐字同序。 */
87
- export declare const MEMORY_ENGINE_BACKENDS: readonly ["file", "pg", "tidb"];
100
+ /** 记忆持久面方言(→ `MEMORY_ENGINE_BACKEND`)。16.0.0:MySQL 族那一只叫 `mysql`(此前 `tidb` —— 同一只 SQL 店有店词 `mysql` 与方言 `tidb` 两个字;
101
+ * 收成一个字,与 {@link STORE_BACKENDS} 同字;硬破坏,`tidb` 读写两面拒、指路一次性迁移 SS-M12)。次序 file → pg → mysql。 */
102
+ export declare const MEMORY_ENGINE_BACKENDS: readonly ["file", "pg", "mysql"];
88
103
  export type MemoryEngineBackend = (typeof MEMORY_ENGINE_BACKENDS)[number];
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 执行车道 + 存储后端的**词表正本**(7.0.0,M-R2 WorkerSpec 结构块)。纯数据常量,零函数、零依赖(叶模块:
3
- * 本文件不 import 任何东西,`types.ts` 反过来引它)。
2
+ * 执行车道 + 存储后端的**词表正本**(7.0.0,M-R2 WorkerSpec 结构块)。纯数据常量,外加两只纯判官函数(退役车道词的拒句 `retiredLaneWordRefusal`、
3
+ * 车道坐标的空白判官 `isBlankCoordinate`),零依赖(叶模块:本文件不 import 任何东西,`types.ts` 反过来引它)。
4
4
  *
5
5
  * ## 为什么在这里
6
6
  * 车道词、每条车道读哪些 env、哪条车道分租户、存储后端词 —— 这几张表以前在四处各抄一份(引擎的能力表、
@@ -17,6 +17,8 @@
17
17
  * · `tenantSeparation`:同文件 `executionLaneCaps` 每臂的 `tenantSeparation` 位(:209 host / :230 e2b / :249 k8s /
18
18
  * :269 ssh / :288 adb / :308 local-docker / :327 device)。`tenantSeparationBasis`(11.0.0):分租户的那几臂凭什么分 ——
19
19
  * e2b / k8s / local-docker 每任务一只隔离目标(`isolation`),device 是发起者自己的设备(`principal-binding`)。
20
+ * · `singleReplica`(16.0.0):这条车道的执行目标挂在**一个副本**上 —— host 是副本自己的文件系统,device 是设备连上的那一个副本
21
+ * (设备的连接落在一个副本上,跨副本把会话送到它的设备今天没有)。表里有一条这种车道的 worker 只能一个副本(写闸按这一列判,不点名车道词)。
20
22
  * · `requiredEnv ∪ requiredSecretEnv`:这条车道缺了就起不了服的键。最初逐字抄自 `src/config.ts` 的手写表 `REQUIRED_ENVS_BY_LANE`(:1054),
21
23
  * 那张手写表已退役 —— 服务端的必需 env 门按本表行的这两栏拒启,部署面的车道清单读同一行,所以今天**本表就是**必需键的唯一来源。按 `src/config-catalog.ts`
22
24
  * 该键那一行的值类型分栏 —— 类型 `secret` / `opaque`(目录自己「绝不回显值」的两类)进 `*SecretEnv`,其余进 `*Env`。
@@ -56,6 +58,7 @@ const LANE_ROWS = {
56
58
  /** server `config.ts` :4014 臂(无必需键);`execution-lane-caps.ts` :209 `single-user-only`(所有任务落 worker 宿主)。 */
57
59
  host: {
58
60
  tenantSeparation: "single-user-only",
61
+ singleReplica: true,
59
62
  requiredEnv: [],
60
63
  requiredSecretEnv: [],
61
64
  optionalEnv: ["HOST_WORKSPACE_BASE", "HOST_COMMAND_TIMEOUT_MS"],
@@ -66,6 +69,7 @@ const LANE_ROWS = {
66
69
  e2b: {
67
70
  tenantSeparation: "supported",
68
71
  tenantSeparationBasis: "isolation",
72
+ singleReplica: false,
69
73
  requiredEnv: [],
70
74
  requiredSecretEnv: ["E2B_API_KEY"],
71
75
  optionalEnv: ["E2B_TEMPLATE", "E2B_TIMEOUT_MS", "E2B_LIVENESS_MS", "E2B_ALLOW_NET", "E2B_MOUNT_PATH", "E2B_ORPHAN_RECLAIM"],
@@ -78,6 +82,7 @@ const LANE_ROWS = {
78
82
  k8s: {
79
83
  tenantSeparation: "supported",
80
84
  tenantSeparationBasis: "isolation",
85
+ singleReplica: false,
81
86
  requiredEnv: ["K8S_SANDBOX_IMAGE", "K8S_NAMESPACE"],
82
87
  requiredSecretEnv: [],
83
88
  optionalEnv: [
@@ -98,6 +103,7 @@ const LANE_ROWS = {
98
103
  * `execution-lane-caps.ts` :269 `single-user-only`(一台跨任务共享的常驻真机)。 */
99
104
  ssh: {
100
105
  tenantSeparation: "single-user-only",
106
+ singleReplica: false,
101
107
  requiredEnv: ["SSH_HOST", "SSH_USER"],
102
108
  requiredSecretEnv: ["SSH_PRIVATE_KEY"],
103
109
  optionalEnv: ["SSH_PORT", "SSH_MOUNT_PATH", "SSH_HOST_FINGERPRINT", "SSH_KNOWN_HOSTS"],
@@ -106,6 +112,7 @@ const LANE_ROWS = {
106
112
  /** server `config.ts` :4006 臂;`execution-lane-caps.ts` :288 `single-user-only`(一台跨任务共享的设备)。 */
107
113
  adb: {
108
114
  tenantSeparation: "single-user-only",
115
+ singleReplica: false,
109
116
  requiredEnv: ["ADB_SERIAL"],
110
117
  requiredSecretEnv: [],
111
118
  optionalEnv: ["ADB_PATH", "ADB_MOUNT_PATH"],
@@ -116,6 +123,7 @@ const LANE_ROWS = {
116
123
  "local-docker": {
117
124
  tenantSeparation: "supported",
118
125
  tenantSeparationBasis: "isolation",
126
+ singleReplica: false,
119
127
  requiredEnv: ["DOCKER_IMAGE"],
120
128
  requiredSecretEnv: [],
121
129
  optionalEnv: [
@@ -131,12 +139,14 @@ const LANE_ROWS = {
131
139
  ],
132
140
  optionalSecretEnv: ["DOCKER_SANDBOX_ENV"],
133
141
  },
134
- /** server `config.ts` :4055 臂 + `parseDevicePreAuthPerRemoteEnv` :871 / `parseDeviceEnrollTokenTtlEnv` :857(无必需键:设备是运行期
135
- * 连进来的);`execution-lane-caps.ts` :327 `supported`(发起者自己的设备)。 */
142
+ /** server `config.ts` :4055 臂 + `parseDevicePreAuthPerRemoteEnv` :871 / `parseDeviceEnrollTokenTtlEnv` :857;`execution-lane-caps.ts` :327
143
+ * `supported`(发起者自己的设备)。16.0.0:`DEVICE_RELAY_URL`(设备拨哪里)是**必需**坐标 —— 引擎的 device 臂缺它即拒启
144
+ * (`parseDeviceRelayUrlEnv`;此前它是 device 臂读而本表漏登记的键),值只有两种:ws(s) 绝对 URL 或字面 `none`(值的文法见 `./device-relay`)。 */
136
145
  device: {
137
146
  tenantSeparation: "supported",
138
147
  tenantSeparationBasis: "principal-binding",
139
- requiredEnv: [],
148
+ singleReplica: true,
149
+ requiredEnv: ["DEVICE_RELAY_URL"],
140
150
  requiredSecretEnv: [],
141
151
  optionalEnv: [
142
152
  "DEVICE_DELIVERY_TIMEOUT_MS",
@@ -157,16 +167,30 @@ export const EXECUTION_LANES = Object.freeze(EXECUTION_LANE_WORDS.map((lane) =>
157
167
  return Object.freeze({
158
168
  lane,
159
169
  ...tenancy,
170
+ singleReplica: row.singleReplica,
160
171
  requiredEnv: Object.freeze([...row.requiredEnv]),
161
172
  requiredSecretEnv: Object.freeze([...row.requiredSecretEnv]),
162
173
  optionalEnv: Object.freeze([...row.optionalEnv]),
163
174
  optionalSecretEnv: Object.freeze([...row.optionalSecretEnv]),
164
175
  });
165
176
  }));
177
+ /**
178
+ * 车道坐标的**唯一判官**:一个必需坐标「未设 / 空串 / 只含空白」一律算缺。本函数判其中的后两种,且只看**已经有的字符串值**:空串,或只含空白(`String.prototype.trim` 认的空白:空格 / 制表 / 换行 / 不换行空格 / 字节序标记 …)⇒ `true`;
179
+ * 「这一键根本没设」由调用方先判(写闸对没设的键另有一句 `requires coords.<键>`,两种缺法同样被拒)。
180
+ * 写闸对所有车道、所有必需坐标同判这一只;本地文件契约 `RemoteExecSpec`(`config.d/remote-exec.json`)的每个非空串字段(镜像 / 命名空间 / 主机 / 路径 / …)问的也是这一只(16.0.0 起:同一个部署声明在两处说,「空白」不再有两种读法);它只判「是不是空白」,不 trim 值、不管值的格式(格式不在这一条的范围:`" sandboxes "` 不是空白)。
181
+ * 它只答「这个值是不是空白」,不管这个键是必需还是可选:据它**拒不拒**一个坐标,由调用方按键的必需 / 可选决定 —— 写闸只对必需坐标据它拒;个别可选坐标上空串有语义(如 `K8S_RUNTIME_CLASS` 的空串 = 不用 Kata),可选坐标的空串照收。
182
+ *
183
+ * 住在这个零依赖的叶模块里:包根(`export *`)与 `./execution-lanes` 子路径是**同一个函数对象** —— 只想问这一个问题的消费方走子路径,不必冷载入整只契约包桶;
184
+ * 车道表的派生索引与写闸也从这里 import,全包只有这一处函数体。
185
+ */
186
+ export function isBlankCoordinate(value) {
187
+ return value.trim() === "";
188
+ }
166
189
  /** 存储后端(→ `DB_BACKEND`)。= server `src/config-types.ts` `DB_BACKEND_WORDS`(:67)逐字同序。 */
167
190
  export const STORE_BACKENDS = ["mysql", "pg", "local", "memory"];
168
191
  /** session 店(→ `SESSION_BACKEND`)。= server `src/config.ts` :1760 `enumEnv("SESSION_BACKEND", …)` 的词表逐字同序
169
192
  * (`tidb` 不在集内:引擎见之拒启)。 */
170
193
  export const SESSION_BACKENDS = ["memory", "mysql", "auto"];
171
- /** 记忆持久面方言(→ `MEMORY_ENGINE_BACKEND`)。= server `src/config.ts` :3436 闭集 / 目录 :468 `enumValues` 逐字同序。 */
172
- export const MEMORY_ENGINE_BACKENDS = ["file", "pg", "tidb"];
194
+ /** 记忆持久面方言(→ `MEMORY_ENGINE_BACKEND`)。16.0.0:MySQL 族那一只叫 `mysql`(此前 `tidb` —— 同一只 SQL 店有店词 `mysql` 与方言 `tidb` 两个字;
195
+ * 收成一个字,与 {@link STORE_BACKENDS} 同字;硬破坏,`tidb` 读写两面拒、指路一次性迁移 SS-M12)。次序 file → pg → mysql。 */
196
+ export const MEMORY_ENGINE_BACKENDS = ["file", "pg", "mysql"];
@@ -1,4 +1,5 @@
1
1
  import { type EffectiveReadWarning } from "./config-fns.js";
2
+ import type { ConfigDocumentRefusal } from "./config-document-refusal.js";
2
3
  import type { ConfigReader } from "./reader.js";
3
4
  import { type DomainConfig, type DomainName, type EffectiveConfig, type RetiredDomainName } from "./types.js";
4
5
  export { NON_PORTABLE_DOMAIN_REASONS, PORTABLE_DOMAINS, type PortableDomain } from "./portable-domains.js";
@@ -21,6 +22,23 @@ export type TolerantEffective = {
21
22
  effective: EffectiveConfig;
22
23
  domainErrors: DomainReadError[];
23
24
  };
25
+ /** 单读口 {@link FileConfigStore.read} 的答案(按 `kind` 判别的闭集两臂;消费方 `switch` 穷举)。两臂都带 `warnings`(读侧告警交成数据,不经 `onWarning`)。 */
26
+ export type FileConfigRead =
27
+ /** 每份文档都读得进:`effective` 与严读 `getEffective()` 逐位同(同一份快照、同一个版本);`warnings` = 没人读的文件 + 装配时的读侧告警。 */
28
+ {
29
+ readonly kind: "effective";
30
+ readonly effective: EffectiveConfig;
31
+ readonly warnings: readonly EffectiveReadWarning[];
32
+ }
33
+ /** 至少一份文档在文件层被拒:`refusals` = 整棵目录的拒单(次序确定:retired 域文档在前,按墓碑表的声明次序;便携域按 `PORTABLE_DOMAINS` 的次序);
34
+ * `version` = 这一次读到的整棵目录的内容版本(被拒的文档以坏文档的指纹参与 —— 与宽容读的坏文件哨兵同一只:同样的坏内容同一个数、坏内容变了换数、
35
+ * 绝不与「文件缺席」同数);`warnings` = 没人读的文件(装配没有发生)。 */
36
+ | {
37
+ readonly kind: "refused";
38
+ readonly refusals: readonly ConfigDocumentRefusal[];
39
+ readonly version: number;
40
+ readonly warnings: readonly EffectiveReadWarning[];
41
+ };
24
42
  export declare class FileConfigStore implements ConfigReader {
25
43
  #private;
26
44
  private readonly root;
@@ -37,9 +55,10 @@ export declare class FileConfigStore implements ConfigReader {
37
55
  domainPath(domain: DomainName | RetiredDomainName): string;
38
56
  /** Read+parse one domain (validated, defaults filled), or `undefined` if the domain is ABSENT — matching
39
57
  * the {@link ConfigReader} contract (reader.ts: absent domain → undefined) and the server stores'
40
- * `has(domain) ? parse : undefined`. A present-but-empty file (`{}` / whitespace) IS a set domain → parsed
41
- * with defaults; a missing file is absence → undefined. (getEffective still fills defaults for absent
42
- * domains; getDomain is the per-domain "was this ever set?" probe.) */
58
+ * `has(domain) ? parse : undefined`. Only a MISSING file (ENOENT) is absence → undefined. A present `{}` IS a set
59
+ * domain → parsed with defaults. A present file that is empty or whitespace-only (a half-write) throws as a corrupt file,
60
+ * as do bad JSON, a non-object, an unreadable file and one nested too deep (the same reading as getEffective / readBundle).
61
+ * (getEffective still fills defaults for absent domains; getDomain is the per-domain "was this ever set?" probe.) */
43
62
  getDomain<K extends DomainName>(domain: K): Promise<DomainConfig[K] | undefined>;
44
63
  /** All portable domains → EffectiveConfig. The PAYLOAD and its `version` come from ONE raws snapshot (no
45
64
  * re-read between them — else a concurrent atomic rename could pair payload A with version B and a consumer
@@ -64,4 +83,12 @@ export declare class FileConfigStore implements ConfigReader {
64
83
  /** Stable version number for the current files: same files ⇒ same number ⇒ a consumer's "skip re-apply if
65
84
  * version unchanged" works locally too. */
66
85
  getVersion(): Promise<number>;
86
+ /**
87
+ * **单读口**(16.0.0):一次读整棵 `config.d`(全部便携域 + retired 域的墓碑文档,每份只读一次),交一个结局({@link FileConfigRead})——
88
+ * · 每份文档在文件层都读得进 ⇒ `effective`:与严读 `getEffective()` 逐位同;读侧告警交成数据(没人读的文件 + 装配告警,不经 `onWarning`);
89
+ * · 有文档在文件层被拒(空 / 坏 JSON / 不是对象 / 读不出 / 过深;带事实的 retired 域文档是墓碑拒,带迁移编号)⇒ `refused`:整棵目录的拒单与这一次读到的内容版本。
90
+ * 严读与 `readBundle` 读的是同一只文件读法(`./config-d-file` 的读数)—— 同一种坏形同一个词、同一句话;区别只在渲染:严读遇到第一份坏文档就抛,本方法把整个目录读完、逐份交出。
91
+ * 文件层之外的拒(读得出、过不了门控域的 schema)照严读抛 —— 那是判形,不在拒单里。拒单、版本、effective、告警都出自这一次读到的字节:没有「先抛、再重探」的第二次读。
92
+ */
93
+ read(): Promise<FileConfigRead>;
67
94
  }