@sema-agent/settings-schema 7.0.0 → 8.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 (106) hide show
  1. package/CHANGELOG.md +173 -1
  2. package/README.md +47 -5
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c01-entry-salvage-then-typo/expected.json +5 -0
  5. package/conformance/cases/c02-hosts-clamp-stacked/expected.json +5 -0
  6. package/conformance/cases/c03-literal-dotted-top-key/expected.json +41 -0
  7. package/conformance/cases/c04-all-bad-collection-not-salvaged/expected.json +5 -0
  8. package/conformance/cases/c05-gate-domain-bad-entry-throws/expected.json +5 -0
  9. package/conformance/cases/c06-legacy-governance-lift/expected.json +12 -0
  10. package/conformance/cases/c07-hooks-is-not-a-config-domain/expected.json +5 -0
  11. package/conformance/cases/c08-limits-unknown-carried-64/expected.json +77 -0
  12. package/conformance/cases/c09-limits-unknown-carried-65/expected.json +78 -0
  13. package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +5 -0
  14. package/conformance/cases/c11-unicode-unknown-keys/expected.json +14 -0
  15. package/conformance/cases/c12-prototype-name-top-keys/expected.json +5 -0
  16. package/conformance/cases/c13-models-field-null/expected.json +5 -0
  17. package/conformance/cases/c14-models-field-empty-array/expected.json +16 -0
  18. package/conformance/cases/c15-models-field-number/expected.json +5 -0
  19. package/conformance/cases/c16-deep-nesting-205/expected.json +14 -0
  20. package/conformance/cases/c17-empty-domain-file/expected.json +10 -0
  21. package/conformance/cases/c18-wire-envelope-not-an-object/expected.json +10 -0
  22. package/conformance/cases/c19-wire-envelope-no-version/expected.json +10 -0
  23. package/conformance/cases/c20-governance-absent-lifts/expected.json +12 -0
  24. package/conformance/cases/c21-governance-empty-object-wins/expected.json +13 -0
  25. package/conformance/cases/c22-governance-null-throws/expected.json +5 -0
  26. package/conformance/cases/c23-mixed-validity-models/expected.json +5 -0
  27. package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +29 -0
  28. package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +33 -0
  29. package/conformance/cases/c26-hosts-array-1024/expected.json +7183 -0
  30. package/conformance/cases/c27-hosts-array-1025/expected.json +5 -0
  31. package/conformance/cases/c28-large-document-over-1mb/expected.json +14 -0
  32. package/conformance/cases/c29-salvage-then-domain-refine-fails/expected.json +5 -0
  33. package/conformance/cases/c30-version-triple-a-first/expected.json +29 -0
  34. package/conformance/cases/c31-version-triple-b-changed/expected.json +29 -0
  35. package/conformance/cases/c32-version-triple-a-restored/expected.json +29 -0
  36. package/conformance/cases/c33-limits-nested-carried-key/expected.json +17 -0
  37. package/conformance/cases/c34-skills-content-vs-manifest/expected.json +23 -0
  38. package/conformance/cases/c35-portable-domain-file-is-null/expected.json +5 -0
  39. package/conformance/cases/c36-skills-entry-without-content/expected.json +23 -0
  40. package/conformance/cases/c37-skills-field-null/expected.json +5 -0
  41. package/conformance/cases/c38-remote-exec-is-a-non-domain-file/expected.json +5 -0
  42. package/conformance/cases/c39-hidden-file-in-config-d/expected.json +5 -0
  43. package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +5 -0
  44. package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +5 -0
  45. package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +33 -0
  46. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +5 -0
  47. package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +5 -0
  48. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +5 -0
  49. package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/expected.json +5 -0
  50. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/expected.json +5 -0
  51. package/conformance/cases/c48-s935-model-headers-transported/expected.json +59 -0
  52. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +5 -0
  53. package/conformance/cases/c50-s784-lane-host-legal/expected.json +44 -0
  54. package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +46 -0
  55. package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +48 -0
  56. package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +48 -0
  57. package/conformance/cases/c54-s784-lane-adb-legal/expected.json +44 -0
  58. package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +45 -0
  59. package/conformance/cases/c56-s784-lane-device-legal/expected.json +51 -0
  60. package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +5 -0
  61. package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +5 -0
  62. package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +5 -0
  63. package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +5 -0
  64. package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +5 -0
  65. package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +5 -0
  66. package/conformance/cases/c63-s784-host-two-replicas/expected.json +5 -0
  67. package/conformance/cases/c64-s784-multitenant-local-store/expected.json +5 -0
  68. package/conformance/cases/c65-s784-pg-without-db/expected.json +5 -0
  69. package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +5 -0
  70. package/conformance/cases/c67-s784-retired-field-loud/expected.json +5 -0
  71. package/conformance/cases/c67-s784-retired-field-loud/meta.json +1 -1
  72. package/conformance/cases/c68-s784-device-local-store/expected.json +5 -0
  73. package/conformance/cases/cc01-mcp-toolfaces-non-object/expected.json +54 -0
  74. package/conformance/cases/cc02-mcp-source-key-is-transported/expected.json +27 -0
  75. package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +33 -0
  76. package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +5 -0
  77. package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +5 -0
  78. package/conformance/cases/cli03-credential-pasted-into-unknown-key/expected.json +22 -0
  79. package/conformance/cases/cli04-env-value-with-newline/expected.json +46 -0
  80. package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +53 -0
  81. package/conformance/cases/t01-absent-domain-key-not-null/expected.json +5 -0
  82. package/conformance/cases/t02-entry-salvage-vs-whole-domain-refusal/expected.json +5 -0
  83. package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +29 -0
  84. package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +5 -0
  85. package/conformance/cases/z01-every-domain-empty-object/expected.json +64 -0
  86. package/conformance/cases/z02-every-domain-unknown-key/expected.json +66 -0
  87. package/dist/cli.js +97 -25
  88. package/dist/config-fns.js +7 -1
  89. package/dist/conformance/contract-adapter.d.ts +4 -2
  90. package/dist/conformance/contract-adapter.js +21 -7
  91. package/dist/conformance/corpus.js +12 -7
  92. package/dist/conformance/derive.d.ts +26 -0
  93. package/dist/conformance/derive.js +25 -2
  94. package/dist/conformance/index.d.ts +4 -3
  95. package/dist/conformance/index.js +4 -3
  96. package/dist/conformance/run.d.ts +3 -3
  97. package/dist/conformance/run.js +87 -17
  98. package/dist/conformance/types.d.ts +57 -17
  99. package/dist/conformance/types.js +4 -2
  100. package/dist/execution-lane-index.d.ts +20 -1
  101. package/dist/execution-lane-index.js +24 -1
  102. package/dist/hooks.d.ts +4 -4
  103. package/dist/migrate-workers.d.ts +1 -1
  104. package/dist/migrate-workers.js +3 -7
  105. package/dist/types.js +13 -4
  106. package/package.json +2 -1
@@ -1,6 +1,7 @@
1
1
  /**
2
- * **三腿等价先定义再比**:语料以 `domains`(各域原始文档)为真源,`wire` 与 `local-root` 两腿的**入参**
3
- * 与**期望**都由本文件派生 —— 派生规则就是三条腿之间那几处**表示差异**的完整登记表。
2
+ * **读腿等价先定义再比**:语料以 `domains`(各域原始文档)为真源,`wire` 与 `local-root` 两腿的**入参**
3
+ * 与**期望**都由本文件派生 —— 派生规则就是三条读腿之间那几处**表示差异**的完整登记表(第四条腿
4
+ * `strictWrite` 的立场见下文与 {@link LEG_EXPECTATION})。
4
5
  *
5
6
  * 为什么派生期望而不是每腿各写一份 golden:三份手写 golden 之间的关系是**隐含**的,谁也说不清某一处
6
7
  * 不同是「设计如此」还是「某一腿读错了」。派生把那层关系变成代码:凡不能由登记的变换解释的差异,一定
@@ -27,6 +28,11 @@
27
28
  *
28
29
  * 语料可以对某一腿写**显式期望**(带 `why`)来覆盖派生 —— 那是「这条 case 的这条腿不由变换解释」的
29
30
  * 明示声明,不是一条暗门。
31
+ *
32
+ * **每条腿的期望从哪来**是一张闭集表({@link LEG_EXPECTATION},`Record<LegName, …>` —— 新开一条腿不来这里
33
+ * 表态就编译红)。`strictWrite`(严写受理)的**入参**与 `domains` 恒等(同一份各域原始文档),**期望不派生**:
34
+ * 严写的结局不是读结局的一种表示变换,而是另一个判断(读腿抢救的地方严写拒)。把它写成「从 domains 期望派生」
35
+ * 就等于让 domains 金样的一处错同时改写两条腿的期望 —— 两条腿同错、门照绿。它的金样逐条显式入库、各自冻结。
30
36
  */
31
37
  import { NON_DOMAIN_CONFIG_FILES, PORTABLE_DOMAINS } from "../file-store.js";
32
38
  import { skillContentHash } from "../skills-manifest.js";
@@ -40,6 +46,23 @@ export const DERIVATION_TRANSFORMS = [
40
46
  { id: "T5-file-layer-first", legs: ["localRoot"], what: "config.d/<域>.json 必须是 JSON 对象(空/null/数组/标量 throw),这一层在域解析之前" },
41
47
  { id: "T6-file-name-classification", legs: ["localRoot"], what: "config.d/ 的文件分三类:域文件 / NON_DOMAIN_CONFIG_FILES(remote-exec.json 自带读取与校验路径)/ 隐藏文件与非 .json(忽略);只有剩下的才报 not-a-domain" },
42
48
  ];
49
+ /**
50
+ * 每条腿的期望从哪来 —— 闭集表态(`Record<LegName, …>`:新开一条腿不在这里表态 ⇒ 编译红)。
51
+ *
52
+ * · `derivable`:语料可以写 `{"derive":true}`,由上面的变换表从 `domains` 期望派生(也可写显式期望并带 `why`)。
53
+ * · `explicit`:只收显式金样(`{"derive":true}` 加载即拒)。
54
+ *
55
+ * 语料加载器与驱动都读这张表(一条规则),不再各自特判某个腿名。
56
+ */
57
+ export const LEG_EXPECTATION = {
58
+ domains: { source: "explicit", why: "真源:domains 腿的期望就是金样本身" },
59
+ strictWrite: {
60
+ source: "explicit",
61
+ why: "入参与 domains 恒等,期望不派生:严写结局不是读结局的表示变换(读腿抢救处严写拒);从 domains 期望派生会让一份金样的错同时改写两条腿",
62
+ },
63
+ wire: { source: "derivable", why: "T1 / T2 / T4" },
64
+ localRoot: { source: "derivable", why: "T3 / T4 / T5 / T6" },
65
+ };
43
66
  /** 非便携域 —— 由 {@link PORTABLE_DOMAINS} 派生,零手抄。 */
44
67
  export const NON_PORTABLE_DOMAINS = DOMAINS.filter((d) => !PORTABLE_DOMAINS.includes(d));
45
68
  const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * 三步接线(消费方侧):
8
8
  * 1. 造一个 {@link ConformanceAdapter}:`provenance` = 你**实际加载到**的那份契约包(package.json 路径、
9
- * 版号、完整性串);`legs` = 你自己的生产读腿,每条带 `entry{module,export}`(生产入口的绝对路径)。
9
+ * 版号、完整性串);`legs` = **四条腿逐条表态**:你自己的生产入口(每条带 `entry{module,export}`,生产入口的
10
+ * 绝对路径),或 `{ declined: "no-production-path", why }`(你的生产代码结构上没有这种读法 / 写法)。
10
11
  * 2. `const report = await runConformance(adapter, { mode: "release" })`。
11
12
  * 3. 断言 `report.ok`;不绿就读 `report.violations` 与每格的 `diff`(effective 的 JSON patch、告警多重集差、
12
13
  * 元数据差三面分开)。
@@ -16,7 +17,7 @@
16
17
  export { runConformance, materializeLocalRoot, resolveLegExpectation } from "./run.js";
17
18
  export { createContractAdapter, type ContractAdapterOptions, type ContractModule } from "./contract-adapter.js";
18
19
  export { loadCorpus, loadCase, kitIdentity, DEFAULT_CORPUS_DIR, PACKAGE_ROOT, type Corpus, type ConformanceCase, type CaseInput, type CaseMeta, type LegExpectationJson } from "./corpus.js";
19
- export { DERIVATION_TRANSFORMS, NON_PORTABLE_DOMAINS, EPOCH_ISO, deriveWireInput, deriveLocalInput, deriveWireExpectation, deriveLocalExpectation, foreignKeysOf, toManifestDoc, type DerivationTransform, type LegExpectation, } from "./derive.js";
20
+ export { DERIVATION_TRANSFORMS, LEG_EXPECTATION, NON_PORTABLE_DOMAINS, EPOCH_ISO, deriveWireInput, deriveLocalInput, deriveWireExpectation, deriveLocalExpectation, foreignKeysOf, toManifestDoc, type DerivationTransform, type LegExpectation, } from "./derive.js";
20
21
  export { canonicalize, deepEqual, diffJson, stableKey, type JsonPatchOp } from "./canonical.js";
21
22
  export { WARNING_KINDS, projectWarning, normalizeProjection, projectionKey, projectionValue, diffWarningMultiset, findSecretLeaks, type WarningKind, type WarningProjection, } from "./warnings.js";
22
- export { LEG_NAMES, REJECT_STAGES, type ConformanceAdapter, type ConformanceLeg, type ConformanceMode, type ConformanceNormalizer, type ConformanceOptions, type ConformanceProvenance, type ConformanceReport, type ConformanceResult, type ConformanceStatus, type KnownDivergence, type OutcomeDivergence, type PathDivergence, type LedgerRow, type LegEntry, type LegInput, type LegName, type LegOutcome, type RejectStage, } from "./types.js";
23
+ export { LEG_DECLINE_REASONS, LEG_NAMES, REJECT_STAGES, type ConformanceAdapter, type ConformanceLeg, type DomainDocsInput, type LegDeclineReason, type LegDeclined, type LegStance, type ConformanceMode, type ConformanceNormalizer, type ConformanceOptions, type ConformanceProvenance, type ConformanceReport, type ConformanceResult, type ConformanceStatus, type KnownDivergence, type OutcomeDivergence, type PathDivergence, type LedgerRow, type LegEntry, type LegInput, type LegName, type LegOutcome, type RejectStage, } from "./types.js";
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * 三步接线(消费方侧):
8
8
  * 1. 造一个 {@link ConformanceAdapter}:`provenance` = 你**实际加载到**的那份契约包(package.json 路径、
9
- * 版号、完整性串);`legs` = 你自己的生产读腿,每条带 `entry{module,export}`(生产入口的绝对路径)。
9
+ * 版号、完整性串);`legs` = **四条腿逐条表态**:你自己的生产入口(每条带 `entry{module,export}`,生产入口的
10
+ * 绝对路径),或 `{ declined: "no-production-path", why }`(你的生产代码结构上没有这种读法 / 写法)。
10
11
  * 2. `const report = await runConformance(adapter, { mode: "release" })`。
11
12
  * 3. 断言 `report.ok`;不绿就读 `report.violations` 与每格的 `diff`(effective 的 JSON patch、告警多重集差、
12
13
  * 元数据差三面分开)。
@@ -16,7 +17,7 @@
16
17
  export { runConformance, materializeLocalRoot, resolveLegExpectation } from "./run.js";
17
18
  export { createContractAdapter } from "./contract-adapter.js";
18
19
  export { loadCorpus, loadCase, kitIdentity, DEFAULT_CORPUS_DIR, PACKAGE_ROOT } from "./corpus.js";
19
- export { DERIVATION_TRANSFORMS, NON_PORTABLE_DOMAINS, EPOCH_ISO, deriveWireInput, deriveLocalInput, deriveWireExpectation, deriveLocalExpectation, foreignKeysOf, toManifestDoc, } from "./derive.js";
20
+ export { DERIVATION_TRANSFORMS, LEG_EXPECTATION, NON_PORTABLE_DOMAINS, EPOCH_ISO, deriveWireInput, deriveLocalInput, deriveWireExpectation, deriveLocalExpectation, foreignKeysOf, toManifestDoc, } from "./derive.js";
20
21
  export { canonicalize, deepEqual, diffJson, stableKey } from "./canonical.js";
21
22
  export { WARNING_KINDS, projectWarning, normalizeProjection, projectionKey, projectionValue, diffWarningMultiset, findSecretLeaks, } from "./warnings.js";
22
- export { LEG_NAMES, REJECT_STAGES, } from "./types.js";
23
+ export { LEG_DECLINE_REASONS, LEG_NAMES, REJECT_STAGES, } from "./types.js";
@@ -19,9 +19,9 @@ export declare function resolveLegExpectation(c: ConformanceCase, leg: LegName,
19
19
  /**
20
20
  * 跑一遍门。
21
21
  *
22
- * `release`:版本三方对齐(适配器加载到的包 == 语料 VERSION == kit 所在包)+ 每条登记的腿都指向一个
23
- * **真存在、且不在契约包 node_modules 之下**的生产入口 + 禁 `only` + 至少登记一条腿 + **登记的腿必须跑**
24
- * (未登记的腿只记 `skipped: no adapter`,不算违例)+ 账本条目的形 + 零陈腐账本条目。
22
+ * `release`:版本三方对齐(适配器加载到的包 == 语料 VERSION == kit 所在包)+ **每条腿都有表态**(实现,或显式声明
23
+ * 不承担:闭集理由词 + why;缺表态 / 形不对 = 违例)+ 每条实现了的腿都指向一个**真存在、且不在契约包自己身上**的生产入口
24
+ * + 禁 `only` + 至少实现一条腿 + **实现的腿必须跑** + 账本条目的形 + 零陈腐账本条目。声明不承担的腿逐格记 `skipped`(带理由)。
25
25
  * `historical`:对旧版包跑,只记行为 diff;归一器可在场且必须自陈有损。
26
26
  */
27
27
  export declare function runConformance(adapter: ConformanceAdapter, opts: ConformanceOptions): Promise<ConformanceReport>;
@@ -19,7 +19,7 @@ import { diffWarningMultiset, findSecretLeaks, projectWarning } from "./warnings
19
19
  import { deriveLocalExpectation, deriveLocalInput, deriveWireExpectation, deriveWireInput, toManifestDoc } from "./derive.js";
20
20
  import { kitIdentity, loadCorpus } from "./corpus.js";
21
21
  import { contractOwnedEntry, contractOwnedEntryWhy } from "./entry-origin.js";
22
- import { LEG_NAMES, } from "./types.js";
22
+ import { LEG_DECLINE_REASONS, LEG_NAMES, } from "./types.js";
23
23
  /** 一个**模块**文件的样子 —— 来源律用它挡住「随便找个存在的文件当入口」(`package.json` 也是文件)。 */
24
24
  const MODULE_EXTENSIONS = [".ts", ".mts", ".cts", ".tsx", ".js", ".mjs", ".cjs", ".jsx"];
25
25
  /**
@@ -77,6 +77,7 @@ function legInput(c, leg) {
77
77
  const { version, updatedAt } = c.input.meta;
78
78
  switch (leg) {
79
79
  case "domains":
80
+ case "strictWrite": // 两条腿吃同一份真源(恒等,不派生入参)
80
81
  return { leg, docs: c.input.domains, version, updatedAt };
81
82
  case "wire":
82
83
  return c.input.wirePayload === undefined ? deriveWireInput(c.input.domains, version, updatedAt) : { leg, payload: c.input.wirePayload };
@@ -254,6 +255,31 @@ function judge(c, leg, expected, actual, normalizer, ledger, hits) {
254
255
  }
255
256
  return { ...base, status: "diff", detail, diff };
256
257
  }
258
+ function stanceOf(adapter, leg) {
259
+ const legs = adapter.legs;
260
+ const s = legs !== null && typeof legs === "object" ? legs[leg] : undefined;
261
+ if (s === undefined)
262
+ return { kind: "missing" };
263
+ if (s === null || typeof s !== "object")
264
+ return { kind: "malformed", why: "the stance is not an object" };
265
+ const runs = typeof s.run === "function";
266
+ const declines = Object.prototype.hasOwnProperty.call(s, "declined");
267
+ // 与类型同律(两臂互斥):声明臂上出现实现臂的任一字段(`entry` / `run`)都算「既实现又声明」。
268
+ const carriesImplementation = runs || Object.prototype.hasOwnProperty.call(s, "entry") || Object.prototype.hasOwnProperty.call(s, "run");
269
+ if (declines && carriesImplementation)
270
+ return { kind: "malformed", why: "it both implements the leg (`entry` / `run`) and declines it (`declined`) — 一条腿只能有一种表态" };
271
+ if (runs)
272
+ return { kind: "implemented", impl: s };
273
+ if (!declines)
274
+ return { kind: "malformed", why: "it neither implements the leg (`run` + `entry`) nor declines it (`declined` + `why`)" };
275
+ const { declined, why } = s;
276
+ if (!LEG_DECLINE_REASONS.includes(declined)) {
277
+ return { kind: "malformed", why: `decline reason ${JSON.stringify(declined)} is not one of ${LEG_DECLINE_REASONS.join(" | ")}` };
278
+ }
279
+ if (typeof why !== "string" || why.trim().length === 0)
280
+ return { kind: "malformed", why: "a declined leg has an empty `why` — 说不出「为什么结构上没有」的一句声明等于没声明" };
281
+ return { kind: "declined", declined: { declined: declined, why } };
282
+ }
257
283
  /** release 模式的**来源律 + 版本护栏**(historical 模式不跑这几条:它就是拿旧版包比行为的)。 */
258
284
  async function releaseViolations(adapter, opts, corpus, kit) {
259
285
  const v = [];
@@ -307,9 +333,30 @@ async function releaseViolations(adapter, opts, corpus, kit) {
307
333
  if (!Array.isArray(cases) || cases.length === 0)
308
334
  v.push(`known divergence \`${e.ref}\` is an outcome divergence that must name at least one case — 结局形不许按前缀大包一整条腿的拒绝面`);
309
335
  }
310
- const legs = Object.entries(adapter.legs).filter(([, l]) => l !== undefined);
336
+ // **每条腿都要表态**:实现,或显式声明不承担(闭集理由词 + why)。缺表态 / 形不对 ⇒ 违例 —— 不许静默跳过,也不许默认当通过。
337
+ const legs = [];
338
+ for (const leg of LEG_NAMES) {
339
+ const s = stanceOf(adapter, leg);
340
+ switch (s.kind) {
341
+ case "implemented":
342
+ legs.push([leg, s.impl]);
343
+ break;
344
+ case "declined":
345
+ break;
346
+ case "missing":
347
+ v.push(`leg \`${leg}\` has no stance — 每条腿要么实现、要么显式声明不承担(\`{ declined: ${LEG_DECLINE_REASONS.map((r) => JSON.stringify(r)).join(" | ")}, why }\`);不表态不是一种表态`);
348
+ break;
349
+ case "malformed":
350
+ v.push(`leg \`${leg}\` has a malformed stance: ${s.why}`);
351
+ break;
352
+ default: {
353
+ const unhandled = s;
354
+ throw new Error(`conformance: unhandled stance ${JSON.stringify(unhandled)}`);
355
+ }
356
+ }
357
+ }
311
358
  if (legs.length === 0)
312
- v.push("adapter registers no leg at all");
359
+ v.push("adapter implements no leg at all");
313
360
  for (const [name, leg] of legs) {
314
361
  const mod = leg.entry.module;
315
362
  if (!isAbsolute(mod)) {
@@ -338,18 +385,34 @@ async function releaseViolations(adapter, opts, corpus, kit) {
338
385
  }
339
386
  return v;
340
387
  }
388
+ /** 一格「没跑」的那句话:按表态说清是哪一种没跑(实现了的腿走不到这里)。 */
389
+ function notRunDetail(s) {
390
+ switch (s.kind) {
391
+ case "declined":
392
+ return `consumer declines this leg (${s.declined.declined}): ${s.declined.why}`;
393
+ case "missing":
394
+ return "no stance: this consumer neither implements nor declines this leg";
395
+ case "malformed":
396
+ return `malformed stance: ${s.why}`;
397
+ default: {
398
+ const unhandled = s;
399
+ throw new Error(`conformance: unhandled stance ${JSON.stringify(unhandled)}`);
400
+ }
401
+ }
402
+ }
341
403
  /**
342
404
  * 跑一遍门。
343
405
  *
344
- * `release`:版本三方对齐(适配器加载到的包 == 语料 VERSION == kit 所在包)+ 每条登记的腿都指向一个
345
- * **真存在、且不在契约包 node_modules 之下**的生产入口 + 禁 `only` + 至少登记一条腿 + **登记的腿必须跑**
346
- * (未登记的腿只记 `skipped: no adapter`,不算违例)+ 账本条目的形 + 零陈腐账本条目。
406
+ * `release`:版本三方对齐(适配器加载到的包 == 语料 VERSION == kit 所在包)+ **每条腿都有表态**(实现,或显式声明
407
+ * 不承担:闭集理由词 + why;缺表态 / 形不对 = 违例)+ 每条实现了的腿都指向一个**真存在、且不在契约包自己身上**的生产入口
408
+ * + 禁 `only` + 至少实现一条腿 + **实现的腿必须跑** + 账本条目的形 + 零陈腐账本条目。声明不承担的腿逐格记 `skipped`(带理由)。
347
409
  * `historical`:对旧版包跑,只记行为 diff;归一器可在场且必须自陈有损。
348
410
  */
349
411
  export async function runConformance(adapter, opts) {
350
412
  const corpus = await loadCorpus(opts.corpusDir);
351
413
  const kit = await kitIdentity();
352
414
  const violations = opts.mode === "release" ? await releaseViolations(adapter, opts, corpus, kit) : [];
415
+ const stances = new Map(LEG_NAMES.map((leg) => [leg, stanceOf(adapter, leg)]));
353
416
  const only = opts.only === undefined ? undefined : new Set(opts.only);
354
417
  const cases = only === undefined ? corpus.cases : corpus.cases.filter((c) => only.has(c.id));
355
418
  if (only !== undefined) {
@@ -368,17 +431,16 @@ export async function runConformance(adapter, opts) {
368
431
  results.push({ caseId: c.id, leg, status: "skipped", detail: `case declares this leg not applicable: ${spec.why}` });
369
432
  continue;
370
433
  }
371
- const impl = adapter.legs[leg];
372
- if (impl === undefined) {
373
- // 未登记 = 这个消费方结构上没有这条读法(docs/CONFORMANCE.md §1「没有的腿不要造」)。逐格记
374
- // `skipped: no adapter`,覆盖矩阵看得见它没跑,**不算违例** —— 律指向的是**登记过的东西**
375
- // (至少一条腿 + 登记的腿必须跑),而不是「三条腿必须都有」。
376
- results.push({ caseId: c.id, leg, status: "skipped", detail: "no adapter: this consumer registered no such read leg" });
434
+ const stance = stances.get(leg);
435
+ if (stance.kind !== "implemented") {
436
+ // 声明不承担 ⇒ 逐格 `skipped`,detail 带理由词与 why(覆盖矩阵看得见它没跑、为什么没跑)。缺表态 / 形不对同样
437
+ // 没法跑 ⇒ 也记 `skipped`,但 release 门已把它记成违例(见 releaseViolations)—— 不表态不是一种表态。
438
+ results.push({ caseId: c.id, leg, status: "skipped", detail: notRunDetail(stance) });
377
439
  continue;
378
440
  }
379
441
  let actual;
380
442
  try {
381
- actual = await impl.run(legInput(c, leg));
443
+ actual = await stance.impl.run(legInput(c, leg));
382
444
  }
383
445
  catch (e) {
384
446
  results.push({ caseId: c.id, leg, status: "harness-error", detail: `the adapter threw instead of returning an outcome: ${e instanceof Error ? e.message : String(e)}` });
@@ -394,12 +456,12 @@ export async function runConformance(adapter, opts) {
394
456
  ? { ref: e.ref, kind: e.kind, leg: e.leg, pathPrefix: e.pathPrefix, ...(e.cases === undefined ? {} : { cases: e.cases }), flipWhen: e.flipWhen, hits: hits[i] }
395
457
  : { ref: e.ref, kind: e.kind, leg: e.outcome.leg, outcome: { expected: e.outcome.expected, actual: e.outcome.actual }, cases: e.outcome.cases, flipWhen: e.flipWhen, hits: hits[i] });
396
458
  if (opts.mode === "release") {
397
- // **登记的腿必须跑**:登记了却在整套语料上一格真读数都没有,那条登记是空的。
459
+ // **实现的腿必须跑**:实现了却在整套语料上一格真读数都没有,那份实现是空的。
398
460
  for (const leg of LEG_NAMES) {
399
- if (adapter.legs[leg] === undefined)
461
+ if (stances.get(leg).kind !== "implemented")
400
462
  continue;
401
463
  if (!results.some((r) => r.leg === leg && r.status !== "skipped")) {
402
- violations.push(`leg \`${leg}\` is registered yet never ran — 整套语料上一格真读数都没有(登记的腿必须跑)`);
464
+ violations.push(`leg \`${leg}\` is implemented yet never ran — 整套语料上一格真读数都没有(实现的腿必须跑)`);
403
465
  }
404
466
  }
405
467
  // **棘轮只降不升**:账本里一条从未命中的登记,自己就是违例 —— 债还了就得把账本删掉。
@@ -411,7 +473,15 @@ export async function runConformance(adapter, opts) {
411
473
  const uniqueViolations = [...new Set(violations)];
412
474
  return {
413
475
  mode: opts.mode,
414
- adapter: { name: adapter.name, provenance: adapter.provenance, legs: LEG_NAMES.filter((l) => adapter.legs[l] !== undefined) },
476
+ adapter: {
477
+ name: adapter.name,
478
+ provenance: adapter.provenance,
479
+ legs: LEG_NAMES.filter((l) => stances.get(l).kind === "implemented"),
480
+ declined: LEG_NAMES.flatMap((leg) => {
481
+ const s = stances.get(leg);
482
+ return s.kind === "declined" ? [{ leg, ...s.declined }] : [];
483
+ }),
484
+ },
415
485
  corpusVersion: corpus.version,
416
486
  kitVersion: kit.version,
417
487
  ...(opts.normalizer === undefined ? {} : { normalizer: { id: opts.normalizer.id, lossy: opts.normalizer.lossy, why: opts.normalizer.why } }),
@@ -10,21 +10,31 @@
10
10
  * 复述的规范化就是第二份真源,而两份真源的分歧正是本门要抓的那类病。
11
11
  */
12
12
  import type { EffectiveReadWarning, WarningProjection } from "../config-fns.js";
13
- /** 三条读腿的名字。**闭集** —— 新开一条腿要同时在语料派生表(`derive.ts` 的 `DERIVATION_TRANSFORMS`)里表态。
13
+ /** 四条腿的名字。**闭集** —— 新开一条腿要同时在 `derive.ts` 的 {@link LEG_EXPECTATION}(`Record<LegName, …>`,
14
+ * 不表态即编译红)里说清它的期望从哪来,且每个消费方的 {@link ConformanceAdapter.legs} 都要对它表态(实现或声明不承担)。
14
15
  *
15
- * · `domains`:各域**原始文档**(center 侧存储形)→ effective。契约仓的 `buildEffective`。
16
+ * · `domains`:各域**原始文档**(center 侧存储形)→ effective。契约仓的 `buildEffective`(读:逐条抢救 + 降级 + 告警)。
17
+ * · `strictWrite`:同一份各域原始文档 → **严写受理**(写入席收下这份文档集与否)。契约仓的 `parseDomain`;管理面的
18
+ * 域写受理。结局复用 {@link LegOutcome} 的两类:全部受理 ⇒ `effective` = 严格解析出的域文档集(只含入参里的键,
19
+ * 不补缺席域);任何一份不受理 ⇒ `rejected{stage:"validate"}`。读腿抢救的(坏条目丢弃、整域降级、主机名夹紧)
20
+ * 在这条腿上是拒 —— 写入席不替运维删他的条目。
16
21
  * · `wire`:`GET /effective` 载荷 → effective。契约仓的 `readEffectiveWire`。
17
22
  * · `localRoot`:`config.d/` 目录快照(文件名 → 文件内容)→ effective。契约仓的 `loadLocalConfig`。 */
18
- export type LegName = "domains" | "wire" | "localRoot";
19
- /** 三条腿的名字表(运行期枚举用;与 {@link LegName} 同源 —— 多一员少一员编译红)。 */
20
- export declare const LEG_NAMES: readonly ["domains", "wire", "localRoot"];
21
- /** 一条读腿的入参。腿名决定形,`domains` 是真源,另两形由 harness 派生(见 derive.ts 的变换表)。 */
22
- export type LegInput = {
23
- leg: "domains";
23
+ export type LegName = "domains" | "strictWrite" | "wire" | "localRoot";
24
+ /** 四条腿的名字表(运行期枚举用;与 {@link LegName} 同源 —— 多一员少一员编译红)。 */
25
+ export declare const LEG_NAMES: readonly ["domains", "strictWrite", "wire", "localRoot"];
26
+ /** 各域原始文档集 —— `domains` 与 `strictWrite` 两条腿**同一份**入参形(真源;一份字段表,两臂共用)。 */
27
+ export interface DomainDocsInput {
24
28
  docs: Readonly<Record<string, unknown>>;
25
29
  version: number;
26
30
  updatedAt: string;
27
- } | {
31
+ }
32
+ /** 一条腿的入参。腿名决定形:`domains` / `strictWrite` 吃真源本身,另两形由 harness 派生(见 derive.ts 的变换表)。 */
33
+ export type LegInput = ({
34
+ leg: "domains";
35
+ } & DomainDocsInput) | ({
36
+ leg: "strictWrite";
37
+ } & DomainDocsInput) | {
28
38
  leg: "wire";
29
39
  payload: unknown;
30
40
  } | {
@@ -95,11 +105,36 @@ export interface LegEntry {
95
105
  /** 该文件里被当作读腿入口的导出名。 */
96
106
  export: string;
97
107
  }
98
- /** 一条登记进 kit 的读腿:它的生产入口 + 真正跑它的那只函数。 */
108
+ /** 一条登记进 kit 的腿:它的生产入口 + 真正跑它的那只函数。与 {@link LegDeclined} **互斥**(类型层与运行期同律):
109
+ * 实现臂不许同时带 `declined`。 */
99
110
  export interface ConformanceLeg {
100
111
  entry: LegEntry;
101
112
  run(input: LegInput): Promise<LegOutcome> | LegOutcome;
113
+ declined?: never;
114
+ }
115
+ /**
116
+ * 「不承担这条腿」的理由。**闭集** —— 一个词就是一条理由,报告里逐格可见,release 门按词判(不认识的词 = 违例)。
117
+ *
118
+ * · `no-production-path`:这个消费方的**生产代码里结构上就没有**这种读法 / 写法 —— 例:worker 只读不写,没有严写受理
119
+ * (`strictWrite`);worker 不从各域原始文档建 effective,只吃 wire 与本地目录(`domains`);管理面不读 `config.d/`(`localRoot`)。
120
+ *
121
+ * 刻意**没有**「有这条路径但还没接」的词:有路径就接(那正是门要比的东西);一份结局上的已知差走 {@link KnownDivergence} 账本,
122
+ * 它带台账号、带翻转条件、受 stale 律管 —— 不必在这里再开一扇不受律管的门。
123
+ */
124
+ export type LegDeclineReason = "no-production-path";
125
+ /** {@link LegDeclineReason} 的运行期表(release 门校验用;与类型同源)。 */
126
+ export declare const LEG_DECLINE_REASONS: readonly ["no-production-path"];
127
+ /** 一条腿的**显式不承担**声明:理由词 + 一句人读的事实(`why`,非空 —— 「为什么结构上没有」要说得出来)。
128
+ * 与 {@link ConformanceLeg} **互斥**:声明臂不许带 `entry` / `run`(从声明迁到实现时 spread 合并留下的旧声明,编译期就红)。 */
129
+ export interface LegDeclined {
130
+ declined: LegDeclineReason;
131
+ why: string;
132
+ entry?: never;
133
+ run?: never;
102
134
  }
135
+ /** 消费方对一条腿的表态:实现它({@link ConformanceLeg}),或声明不承担({@link LegDeclined})。两臂互斥;没有第三种 —— 不表态不是一种表态。
136
+ * release 门在运行期按同一条律判(适配器可能来自 JS / `as`):带 `declined` 又带 `entry` 或 `run` = 形不对。 */
137
+ export type LegStance = ConformanceLeg | LegDeclined;
103
138
  /**
104
139
  * 消费方对金样的一条**已知合法差异**的登记位 —— 临时欠账,或成文语义。
105
140
  *
@@ -164,14 +199,15 @@ export interface ConformanceAdapter {
164
199
  name: string;
165
200
  provenance: ConformanceProvenance;
166
201
  /**
167
- * **只登记你真有的腿**。未登记 = 这个消费方结构上没有这条读法(例:worker 服务的生产代码里没有
168
- * `buildEffective` 调用点,它只有 wire 腿与 local-root 腿)—— 报告逐格记 `skipped: no adapter`
169
- * (覆盖矩阵看得见它没跑),**不算违例**。
202
+ * **每条腿都要表态**(一条规则,四条腿同律):实现它,或显式声明不承担({@link LegDeclined},带理由词)。
203
+ * 类型是**全量** `Record` —— 闭集 {@link LegName} 加一员,每个消费方的适配器就编译红,直到它对新腿表态;
204
+ * 运行期(适配器可能来自 JS / `as`)release 门照判:缺表态 / 形不对 / 理由词不在闭集 / `why` 为空 ⇒ 违例。
205
+ * 声明不承担的腿逐格记 `skipped`(detail 带理由词与 `why`),并列进报告的 `adapter.declined` —— 看得见、不算通过、也不算红。
170
206
  *
171
- * release 模式的两条律都指向**登记过的东西**:①至少登记一条腿;②**登记的腿必须跑** —— 登记了却在
172
- * 整套语料上一格真读数都没有,那条登记是空的,违例。
207
+ * release 模式另两条律指向**实现了的腿**:①至少实现一条腿;②**实现的腿必须跑** —— 实现了却在整套语料上一格真读数都没有,
208
+ * 那份实现是空的,违例。
173
209
  */
174
- legs: Partial<Record<LegName, ConformanceLeg>>;
210
+ legs: Record<LegName, LegStance>;
175
211
  /** 已知的**合法**差异账本(见 {@link KnownDivergence})。缺席 = 零容忍,任何 diff 都阻塞。 */
176
212
  knownDivergences?: readonly KnownDivergence[];
177
213
  }
@@ -200,7 +236,7 @@ export interface ConformanceNormalizer {
200
236
  }
201
237
  /**
202
238
  * 一格结局。`pass` / `expected-rejected` / `diff-ledgered` 不阻塞,`diff` / `harness-error` 红,
203
- * `skipped` 既不绿也不红(没跑)。
239
+ * `skipped` 既不绿也不红(没跑:语料声明本腿不适用,或消费方声明不承担 / 没表态 —— detail 说是哪一种)。
204
240
  *
205
241
  * `diff-ledgered`(3.0.0 车 FT):这一格的 effective 差**逐坐标**落在适配器的
206
242
  * {@link KnownDivergence} 账本里 —— 一笔登记在案的合法差异,不阻塞,但单独计数、按 `ref` 列出来。
@@ -261,10 +297,14 @@ export interface LedgerRow {
261
297
  /** 一次跑的报告。 */
262
298
  export interface ConformanceReport {
263
299
  mode: ConformanceMode;
300
+ /** `legs` = 实现了的腿;`declined` = 显式声明不承担的腿(理由词 + `why`)。两者之外的腿 = 没表态(release 模式违例)。 */
264
301
  adapter: {
265
302
  name: string;
266
303
  provenance: ConformanceProvenance;
267
304
  legs: readonly LegName[];
305
+ declined: readonly ({
306
+ leg: LegName;
307
+ } & LegDeclined)[];
268
308
  };
269
309
  /** 语料版本(`conformance/VERSION`)。 */
270
310
  corpusVersion: string;
@@ -1,4 +1,6 @@
1
- /** 三条腿的名字表(运行期枚举用;与 {@link LegName} 同源 —— 多一员少一员编译红)。 */
2
- export const LEG_NAMES = ["domains", "wire", "localRoot"];
1
+ /** 四条腿的名字表(运行期枚举用;与 {@link LegName} 同源 —— 多一员少一员编译红)。 */
2
+ export const LEG_NAMES = ["domains", "strictWrite", "wire", "localRoot"];
3
3
  /** {@link RejectStage} 的运行期表(语料校验用;与类型同源)。 */
4
4
  export const REJECT_STAGES = ["envelope", "parse", "validate", "hash", "store"];
5
+ /** {@link LegDeclineReason} 的运行期表(release 门校验用;与类型同源)。 */
6
+ export const LEG_DECLINE_REASONS = ["no-production-path"];
@@ -1,7 +1,14 @@
1
1
  /**
2
- * 包内私有:车道表({@link EXECUTION_LANES})的两只**派生索引**,供 `types.ts`(写闸)与 `migrate-workers.ts`(一次性迁移)同读。
2
+ * 包内私有:车道表({@link EXECUTION_LANES})的两只**派生索引** + 「迁移能搬哪些键」的**一份**键集与指路句,
3
+ * 供 `types.ts`(写闸)与 `migrate-workers.ts`(一次性迁移)同读。
3
4
  * 不进主桶、不进 `package.json` 的 `exports`(与 `secret-scan.ts` 同一姿势)—— 消费方只拿表本身,索引是本包的实现细节。
4
5
  * 索引只从表派生,不另存任何词:表改了,这里跟着变。
6
+ *
7
+ * ## 迁移指路(7.0.1)
8
+ * 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)不带**。
9
+ * 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS}),写闸读的是**同一只谓词**
10
+ * ({@link isMigratableEnvKey}),不是第二张手抄的表 —— 迁移多认一个键而写闸没跟,或反过来,不会静默分叉
11
+ * (`test/migration-signpost-7.0.1.test.ts` 用真跑迁移的读数逐个保留名对拍)。
5
12
  */
6
13
  import { type ExecutionLane, type ExecutionLaneRow } from "./execution-lanes.js";
7
14
  /** 一条车道的表行。词表与表行由 `execution-lanes.ts` 的 `satisfies Record<ExecutionLane, …>` 一一对应 ——
@@ -17,3 +24,15 @@ export interface LaneEnvOwner {
17
24
  export declare const LANE_ENV_OWNER: ReadonlyMap<string, LaneEnvOwner>;
18
25
  /** 一个车道 env 名在 WorkerSpec 里的**唯一写位**(保留键的拒因指路用它)。 */
19
26
  export declare function laneEnvSeat(name: string, owner: LaneEnvOwner): string;
27
+ /** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.lane`、`REQUIRE_EXECUTION_LANE` 丢弃)。 */
28
+ export declare const LANE_WORD_KEYS: readonly ["REMOTE_EXEC", "REQUIRE_EXECUTION_LANE"];
29
+ /** 渲染器以前不渲、运维只能写在 env 逃生口里的店键(7.0.0 起归 `runtime.store`;迁移搬进 `store`)。 */
30
+ export declare const STORE_ENV_KEYS: readonly ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BACKEND"];
31
+ /** 一个名字是不是车道块的键:车道词两键,或车道表里的任一车道 env 名。 */
32
+ export declare function isLaneEnvKey(name: string): boolean;
33
+ /** 一个名字是不是迁移会处理的 6.x env 键(搬进结构块 / 丢弃 / 点名它拒):车道块的键 ∪ 店键。写闸拒因带不带迁移指路只看它。 */
34
+ export declare function isMigratableEnvKey(name: string): boolean;
35
+ /** 迁移命令的唯一拼写(design/327 §3.4 原句):`npx` 全名形 —— 没装本包的运维粘贴即可跑;裸 bin 名 `settings-schema …` 在没装本包的机器上是 `command not found`。 */
36
+ export declare const MIGRATE_WORKERS_COMMAND: "npx @sema-agent/settings-schema migrate-workers";
37
+ /** 拒因里的迁移指路 —— **一句话、一个常量**:保留名闸(`env` / `secretEnv` 里的 {@link isMigratableEnvKey} 键)与删除项预处理(七个被删旧键)引用同一个。 */
38
+ export declare const MIGRATION_SIGNPOST: string;
@@ -1,7 +1,14 @@
1
1
  /**
2
- * 包内私有:车道表({@link EXECUTION_LANES})的两只**派生索引**,供 `types.ts`(写闸)与 `migrate-workers.ts`(一次性迁移)同读。
2
+ * 包内私有:车道表({@link EXECUTION_LANES})的两只**派生索引** + 「迁移能搬哪些键」的**一份**键集与指路句,
3
+ * 供 `types.ts`(写闸)与 `migrate-workers.ts`(一次性迁移)同读。
3
4
  * 不进主桶、不进 `package.json` 的 `exports`(与 `secret-scan.ts` 同一姿势)—— 消费方只拿表本身,索引是本包的实现细节。
4
5
  * 索引只从表派生,不另存任何词:表改了,这里跟着变。
6
+ *
7
+ * ## 迁移指路(7.0.1)
8
+ * 拒因要不要带迁移指路只有一条规则:**这个键「旧形 ⇒ 迁移能搬」就带,搬不了的保留名(`PORT` / `MODEL_*` …)不带**。
9
+ * 「迁移能搬」= 迁移自己识别的键集(车道键 {@link isLaneEnvKey} ∪ 店键 {@link STORE_ENV_KEYS}),写闸读的是**同一只谓词**
10
+ * ({@link isMigratableEnvKey}),不是第二张手抄的表 —— 迁移多认一个键而写闸没跟,或反过来,不会静默分叉
11
+ * (`test/migration-signpost-7.0.1.test.ts` 用真跑迁移的读数逐个保留名对拍)。
5
12
  */
6
13
  import { EXECUTION_LANES } from "./execution-lanes.js";
7
14
  /** 一条车道的表行。词表与表行由 `execution-lanes.ts` 的 `satisfies Record<ExecutionLane, …>` 一一对应 ——
@@ -21,3 +28,19 @@ export const LANE_ENV_OWNER = new Map(EXECUTION_LANES.flatMap((r) => {
21
28
  export function laneEnvSeat(name, owner) {
22
29
  return `runtime.execution.${owner.secret ? "secretRefs" : "coords"}.${name}`;
23
30
  }
31
+ /** 车道块的两个特殊键(不在车道表里,但归车道块:迁移把 `REMOTE_EXEC` 搬成 `runtime.execution.lane`、`REQUIRE_EXECUTION_LANE` 丢弃)。 */
32
+ export const LANE_WORD_KEYS = ["REMOTE_EXEC", "REQUIRE_EXECUTION_LANE"];
33
+ /** 渲染器以前不渲、运维只能写在 env 逃生口里的店键(7.0.0 起归 `runtime.store`;迁移搬进 `store`)。 */
34
+ export const STORE_ENV_KEYS = ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BACKEND"];
35
+ /** 一个名字是不是车道块的键:车道词两键,或车道表里的任一车道 env 名。 */
36
+ export function isLaneEnvKey(name) {
37
+ return LANE_WORD_KEYS.some((k) => k === name) || LANE_ENV_OWNER.has(name);
38
+ }
39
+ /** 一个名字是不是迁移会处理的 6.x env 键(搬进结构块 / 丢弃 / 点名它拒):车道块的键 ∪ 店键。写闸拒因带不带迁移指路只看它。 */
40
+ export function isMigratableEnvKey(name) {
41
+ return isLaneEnvKey(name) || STORE_ENV_KEYS.some((k) => k === name);
42
+ }
43
+ /** 迁移命令的唯一拼写(design/327 §3.4 原句):`npx` 全名形 —— 没装本包的运维粘贴即可跑;裸 bin 名 `settings-schema …` 在没装本包的机器上是 `command not found`。 */
44
+ export const MIGRATE_WORKERS_COMMAND = "npx @sema-agent/settings-schema migrate-workers";
45
+ /** 拒因里的迁移指路 —— **一句话、一个常量**:保留名闸(`env` / `secretEnv` 里的 {@link isMigratableEnvKey} 键)与删除项预处理(七个被删旧键)引用同一个。 */
46
+ export const MIGRATION_SIGNPOST = `lift a stored 6.x spec once with migrateWorkersDomain(doc) (on the raw document, ahead of parseDomain) or \`${MIGRATE_WORKERS_COMMAND} <file>\``;
package/dist/hooks.d.ts CHANGED
@@ -1575,26 +1575,26 @@ export declare const MessageDisplayHookInput: z.ZodObject<{
1575
1575
  /** Newly completed lines since the prior flush (final flush may end mid-line / be empty). */
1576
1576
  delta: z.ZodString;
1577
1577
  }, "strip", z.ZodTypeAny, {
1578
+ index: number;
1578
1579
  session_id: string;
1579
1580
  transcript_path: string;
1580
1581
  cwd: string;
1581
1582
  hook_event_name: "MessageDisplay";
1582
1583
  turn_id: string;
1583
1584
  message_id: string;
1584
- index: number;
1585
1585
  final: boolean;
1586
1586
  delta: string;
1587
1587
  agent_type?: string | undefined;
1588
1588
  permission_mode?: string | undefined;
1589
1589
  agent_id?: string | undefined;
1590
1590
  }, {
1591
+ index: number;
1591
1592
  session_id: string;
1592
1593
  transcript_path: string;
1593
1594
  cwd: string;
1594
1595
  hook_event_name: "MessageDisplay";
1595
1596
  turn_id: string;
1596
1597
  message_id: string;
1597
- index: number;
1598
1598
  final: boolean;
1599
1599
  delta: string;
1600
1600
  agent_type?: string | undefined;
@@ -3040,26 +3040,26 @@ export declare const HookInput: z.ZodDiscriminatedUnion<"hook_event_name", [z.Zo
3040
3040
  /** Newly completed lines since the prior flush (final flush may end mid-line / be empty). */
3041
3041
  delta: z.ZodString;
3042
3042
  }, "strip", z.ZodTypeAny, {
3043
+ index: number;
3043
3044
  session_id: string;
3044
3045
  transcript_path: string;
3045
3046
  cwd: string;
3046
3047
  hook_event_name: "MessageDisplay";
3047
3048
  turn_id: string;
3048
3049
  message_id: string;
3049
- index: number;
3050
3050
  final: boolean;
3051
3051
  delta: string;
3052
3052
  agent_type?: string | undefined;
3053
3053
  permission_mode?: string | undefined;
3054
3054
  agent_id?: string | undefined;
3055
3055
  }, {
3056
+ index: number;
3056
3057
  session_id: string;
3057
3058
  transcript_path: string;
3058
3059
  cwd: string;
3059
3060
  hook_event_name: "MessageDisplay";
3060
3061
  turn_id: string;
3061
3062
  message_id: string;
3062
- index: number;
3063
3063
  final: boolean;
3064
3064
  delta: string;
3065
3065
  agent_type?: string | undefined;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * workers 域的**一次性迁移**(7.0.0,M-R2 WorkerSpec 结构块)—— 持久面唯一允许的过渡,窗口关了就整文件删除
3
3
  * (连同 `types.ts` 里「删除项读到即拒」那一只预处理)。纯函数、无 I/O、不改入参;文件形的入口是
4
- * `settings-schema migrate-workers <file>`(`bin/settings-schema.mjs`,同一只函数)。
4
+ * `npx @sema-agent/settings-schema migrate-workers <file>`(`bin/settings-schema.mjs`,同一只函数;命令拼写的唯一正本 = `MIGRATE_WORKERS_COMMAND`)。
5
5
  *
6
6
  * ## 谁跑、在哪
7
7
  * 读前抬升:在 `parseDomain("workers", raw)` / `DOMAIN_SCHEMAS.workers.safeParse` **之前**对**原始 JSON** 跑 —— workers 是
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * workers 域的**一次性迁移**(7.0.0,M-R2 WorkerSpec 结构块)—— 持久面唯一允许的过渡,窗口关了就整文件删除
3
3
  * (连同 `types.ts` 里「删除项读到即拒」那一只预处理)。纯函数、无 I/O、不改入参;文件形的入口是
4
- * `settings-schema migrate-workers <file>`(`bin/settings-schema.mjs`,同一只函数)。
4
+ * `npx @sema-agent/settings-schema migrate-workers <file>`(`bin/settings-schema.mjs`,同一只函数;命令拼写的唯一正本 = `MIGRATE_WORKERS_COMMAND`)。
5
5
  *
6
6
  * ## 谁跑、在哪
7
7
  * 读前抬升:在 `parseDomain("workers", raw)` / `DOMAIN_SCHEMAS.workers.safeParse` **之前**对**原始 JSON** 跑 —— workers 是
@@ -32,16 +32,12 @@
32
32
  * 已迁形(没有任何旧键、该有的块都在)⇒ 原样(同一引用)返回、`changed = []`。所以再跑一次 ⇒ 零变化、`doc` 字节相等。
33
33
  */
34
34
  import { formatConfigPath, redactSecrets } from "./config-fns.js";
35
- import { LANE_ENV_OWNER } from "./execution-lane-index.js";
35
+ import { LANE_ENV_OWNER, STORE_ENV_KEYS, isLaneEnvKey } from "./execution-lane-index.js";
36
36
  import { EXECUTION_LANE_WORDS, MEMORY_ENGINE_BACKENDS, STORE_BACKENDS } from "./execution-lanes.js";
37
37
  import { WorkerSpec } from "./types.js";
38
38
  function isRec(v) {
39
39
  return v !== null && typeof v === "object" && !Array.isArray(v);
40
40
  }
41
- /** 车道块的两个特殊键(不在车道表里,但归车道块)。 */
42
- const LANE_WORD_KEYS = ["REMOTE_EXEC", "REQUIRE_EXECUTION_LANE"];
43
- /** 渲染器以前不渲、运维只能写在 env 逃生口里的店键(7.0.0 起归 `runtime.store`)。 */
44
- const STORE_ENV_KEYS = ["DB_BACKEND", "MEMORY_ENGINE", "MEMORY_ENGINE_BACKEND"];
45
41
  /** 6.x 删除项:`[容器, 键]`。 */
46
42
  const SIX_X_STORE_FIELDS = [
47
43
  ["runtime", "sessionBackend"],
@@ -78,7 +74,7 @@ function migrateEntry(raw, index) {
78
74
  const refuse = (field, reason) => ({ kind: "refused", refusal: { name, index, field, reason: redactSecrets(reason), original: raw } });
79
75
  const enabled = raw.enabled !== false;
80
76
  const has = (container, key) => isRec(container) && Object.hasOwn(container, key);
81
- const laneKeysOf = (rec) => isRec(rec) ? Object.keys(rec).filter((k) => LANE_WORD_KEYS.some((w) => w === k) || LANE_ENV_OWNER.has(k)) : [];
77
+ const laneKeysOf = (rec) => (isRec(rec) ? Object.keys(rec).filter(isLaneEnvKey) : []);
82
78
  const storeKeysOf = (rec) => (isRec(rec) ? STORE_ENV_KEYS.filter((k) => Object.hasOwn(rec, k)) : []);
83
79
  const envLaneKeys = laneKeysOf(raw.env);
84
80
  const secretLaneKeys = laneKeysOf(raw.secretEnv);