@sema-agent/settings-schema 3.0.0 → 4.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 (46) hide show
  1. package/CHANGELOG.md +327 -1
  2. package/README.md +16 -8
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/t01-absent-domain-key-not-null/expected.json +93 -0
  5. package/conformance/cases/t01-absent-domain-key-not-null/input.json +29 -0
  6. package/conformance/cases/t01-absent-domain-key-not-null/meta.json +6 -0
  7. package/conformance/cases/t02-entry-salvage-vs-whole-domain-refusal/expected.json +16 -0
  8. package/conformance/cases/t02-entry-salvage-vs-whole-domain-refusal/input.json +32 -0
  9. package/conformance/cases/t02-entry-salvage-vs-whole-domain-refusal/meta.json +6 -0
  10. package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +107 -0
  11. package/conformance/cases/t03-prototype-name-key-not-swallowed/input.json +25 -0
  12. package/conformance/cases/t03-prototype-name-key-not-swallowed/meta.json +6 -0
  13. package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +105 -0
  14. package/conformance/cases/t04-secret-literal-never-in-warning-value/input.json +25 -0
  15. package/conformance/cases/t04-secret-literal-never-in-warning-value/meta.json +14 -0
  16. package/conformance/historical/settings-schema@1.0.0.json +84 -3
  17. package/conformance/historical/settings-schema@1.10.0.json +84 -3
  18. package/conformance/historical/settings-schema@1.5.0.json +84 -3
  19. package/conformance/historical/settings-schema@2.0.0.json +83 -2
  20. package/conformance/historical/settings-schema@2.1.0.json +83 -2
  21. package/dist/api/auth.d.ts +21 -0
  22. package/dist/api/auth.js +21 -0
  23. package/dist/config-fns.d.ts +76 -11
  24. package/dist/config-fns.js +81 -26
  25. package/dist/conformance/contract-adapter.js +37 -0
  26. package/dist/conformance/entry-origin.d.ts +24 -0
  27. package/dist/conformance/entry-origin.js +86 -0
  28. package/dist/conformance/run.js +6 -6
  29. package/dist/conformance/types.d.ts +5 -1
  30. package/dist/conformance/warnings.d.ts +7 -48
  31. package/dist/conformance/warnings.js +17 -40
  32. package/dist/cross-domain.d.ts +27 -7
  33. package/dist/cross-domain.js +31 -11
  34. package/dist/hooks.d.ts +2 -2
  35. package/dist/hooks.js +2 -2
  36. package/dist/node.d.ts +1 -0
  37. package/dist/node.js +1 -0
  38. package/dist/prompts-digest.d.ts +16 -0
  39. package/dist/prompts-digest.js +48 -0
  40. package/dist/scheduler-store-node.d.ts +7 -19
  41. package/dist/scheduler-store-node.js +8 -26
  42. package/dist/scheduler-store.d.ts +1 -1
  43. package/dist/scheduler-store.js +1 -1
  44. package/dist/types.d.ts +2 -2
  45. package/dist/types.js +3 -3
  46. package/package.json +3 -1
@@ -8,9 +8,10 @@
8
8
  "why": "旧版坐标是点号串、今天是段数组;归一 = 按 `.` 拼串,这正是 3.0.0 要消掉的有损编码(`[\"a.b\"]` 与 `[\"a\",\"b\"]` 归一后同串)"
9
9
  },
10
10
  "summary": {
11
- "pass": 134,
11
+ "pass": 143,
12
12
  "diff": 1,
13
- "expected-rejected": 12,
13
+ "diff-ledgered": 0,
14
+ "expected-rejected": 15,
14
15
  "harness-error": 0,
15
16
  "skipped": 0
16
17
  },
@@ -955,6 +956,86 @@
955
956
  }
956
957
  }
957
958
  },
959
+ "t01-absent-domain-key-not-null": {
960
+ "firstRed": false,
961
+ "legs": {
962
+ "domains": {
963
+ "status": "pass",
964
+ "behaviour": false,
965
+ "detail": "effective, warnings and metadata all match"
966
+ },
967
+ "wire": {
968
+ "status": "pass",
969
+ "behaviour": false,
970
+ "detail": "effective, warnings and metadata all match"
971
+ },
972
+ "localRoot": {
973
+ "status": "pass",
974
+ "behaviour": false,
975
+ "detail": "effective, warnings and metadata all match"
976
+ }
977
+ }
978
+ },
979
+ "t02-entry-salvage-vs-whole-domain-refusal": {
980
+ "firstRed": false,
981
+ "legs": {
982
+ "domains": {
983
+ "status": "expected-rejected",
984
+ "behaviour": false,
985
+ "detail": "rejected at `validate` as expected"
986
+ },
987
+ "wire": {
988
+ "status": "expected-rejected",
989
+ "behaviour": false,
990
+ "detail": "rejected at `validate` as expected"
991
+ },
992
+ "localRoot": {
993
+ "status": "expected-rejected",
994
+ "behaviour": false,
995
+ "detail": "rejected at `validate` as expected"
996
+ }
997
+ }
998
+ },
999
+ "t03-prototype-name-key-not-swallowed": {
1000
+ "firstRed": false,
1001
+ "legs": {
1002
+ "domains": {
1003
+ "status": "pass",
1004
+ "behaviour": false,
1005
+ "detail": "effective, warnings and metadata all match"
1006
+ },
1007
+ "wire": {
1008
+ "status": "pass",
1009
+ "behaviour": false,
1010
+ "detail": "effective, warnings and metadata all match"
1011
+ },
1012
+ "localRoot": {
1013
+ "status": "pass",
1014
+ "behaviour": false,
1015
+ "detail": "effective, warnings and metadata all match"
1016
+ }
1017
+ }
1018
+ },
1019
+ "t04-secret-literal-never-in-warning-value": {
1020
+ "firstRed": false,
1021
+ "legs": {
1022
+ "domains": {
1023
+ "status": "pass",
1024
+ "behaviour": false,
1025
+ "detail": "effective, warnings and metadata all match"
1026
+ },
1027
+ "wire": {
1028
+ "status": "pass",
1029
+ "behaviour": false,
1030
+ "detail": "effective, warnings and metadata all match"
1031
+ },
1032
+ "localRoot": {
1033
+ "status": "pass",
1034
+ "behaviour": false,
1035
+ "detail": "effective, warnings and metadata all match"
1036
+ }
1037
+ }
1038
+ },
958
1039
  "z01-every-domain-empty-object": {
959
1040
  "firstRed": false,
960
1041
  "legs": {
@@ -24,6 +24,27 @@
24
24
  * RS256 JWT (1h) — see `api/auth-bridge` for how a worker verifies it.
25
25
  *
26
26
  * Pure zod contract — zero IO, browser-safe, same rules as the rest of this package.
27
+ *
28
+ * ── 4.0.0:这一面**保留**(不删)。理由 / 谁该改调 / 什么条件下删 ───────────────────────────────
29
+ *
30
+ * 读数(4.0.0 前夜逐行核过,不是按计数下的结论):**没有任何消费仓 import 子路径 `./api/auth`**,
31
+ * 也没有人从本包的桶里取这 18 个名(尺子:同一把 grep 在那几个仓里数 `@sema-agent/settings-schema`
32
+ * 的 import 行,读数分别是几十到上百行 —— 所以那个零是真零,不是 grep 坏了)。**但零消费不等于这面死了**:
33
+ * 上面第 2-4 行那句「大家 import ONE source of truth」今天是**意图**,不是读数 —— 真实情况是各抄了一份:
34
+ * · 管理面(配置控制台)`src/lib/auth/device-flow.ts` 自带一只 `normalizeUserCode`,函数体与本文件那只
35
+ * **逐字节相同**,另有自己的 `DEVICE_GRANT_TYPE` / `DEVICE_POLL_INTERVAL_SECONDS`;
36
+ * · 壳(命令行)`src/sema/cloudAuth.ts` 自声明 `DeviceCodeResponse` / `TokenResponse` 两只响应形;
37
+ * · SDK 在自己的 `registry/auth.ts` 里做**值拷贝**(`REGISTRY_AUTH_PATHS` / grant_type / 错误码集),
38
+ * 并在**生产码**里把 `DeviceCodeResponse` / `TokenGrantResponse` 两只当类型 import —— 但取的是
39
+ * **改名前的那个旧包名**,靠它自己的「同源锚」测试对账;那道锚今天钉的是旧包(devDependency 停在
40
+ * 0.17.1),于是本包改名之后的任何改动它都看不见。
41
+ * (所以上面那句「零 import」的**精确射程**是:没有任何仓从**本包这个名字**下取这 18 个名。)
42
+ *
43
+ * ⇒ 删掉这一面 = 把上面三份手抄就地合法化。凭据轴该有**单属主**,所以 4.0.0 取「迁移」不取「删」:
44
+ * · 谁该改调:管理面与壳直接 import 本面(先从 `normalizeUserCode` / 设备码响应形这几只开始);
45
+ * SDK 把它那道同源锚从旧包名改到本包(锚不动,漂移就永远是绿的)。
46
+ * · 什么条件下删:**下一个 major**,且届时三方都已改调(或各自明确表态不改并自己认账),且那时**仍**
47
+ * 是零消费 —— 那天「删」的理由才成立。今天不成立:今天的零是手抄造出来的,不是这面没人要。
27
48
  */
28
49
  import { z } from "zod";
29
50
  /** The five auth endpoints, relative to the registry origin. */
package/dist/api/auth.js CHANGED
@@ -24,6 +24,27 @@
24
24
  * RS256 JWT (1h) — see `api/auth-bridge` for how a worker verifies it.
25
25
  *
26
26
  * Pure zod contract — zero IO, browser-safe, same rules as the rest of this package.
27
+ *
28
+ * ── 4.0.0:这一面**保留**(不删)。理由 / 谁该改调 / 什么条件下删 ───────────────────────────────
29
+ *
30
+ * 读数(4.0.0 前夜逐行核过,不是按计数下的结论):**没有任何消费仓 import 子路径 `./api/auth`**,
31
+ * 也没有人从本包的桶里取这 18 个名(尺子:同一把 grep 在那几个仓里数 `@sema-agent/settings-schema`
32
+ * 的 import 行,读数分别是几十到上百行 —— 所以那个零是真零,不是 grep 坏了)。**但零消费不等于这面死了**:
33
+ * 上面第 2-4 行那句「大家 import ONE source of truth」今天是**意图**,不是读数 —— 真实情况是各抄了一份:
34
+ * · 管理面(配置控制台)`src/lib/auth/device-flow.ts` 自带一只 `normalizeUserCode`,函数体与本文件那只
35
+ * **逐字节相同**,另有自己的 `DEVICE_GRANT_TYPE` / `DEVICE_POLL_INTERVAL_SECONDS`;
36
+ * · 壳(命令行)`src/sema/cloudAuth.ts` 自声明 `DeviceCodeResponse` / `TokenResponse` 两只响应形;
37
+ * · SDK 在自己的 `registry/auth.ts` 里做**值拷贝**(`REGISTRY_AUTH_PATHS` / grant_type / 错误码集),
38
+ * 并在**生产码**里把 `DeviceCodeResponse` / `TokenGrantResponse` 两只当类型 import —— 但取的是
39
+ * **改名前的那个旧包名**,靠它自己的「同源锚」测试对账;那道锚今天钉的是旧包(devDependency 停在
40
+ * 0.17.1),于是本包改名之后的任何改动它都看不见。
41
+ * (所以上面那句「零 import」的**精确射程**是:没有任何仓从**本包这个名字**下取这 18 个名。)
42
+ *
43
+ * ⇒ 删掉这一面 = 把上面三份手抄就地合法化。凭据轴该有**单属主**,所以 4.0.0 取「迁移」不取「删」:
44
+ * · 谁该改调:管理面与壳直接 import 本面(先从 `normalizeUserCode` / 设备码响应形这几只开始);
45
+ * SDK 把它那道同源锚从旧包名改到本包(锚不动,漂移就永远是绿的)。
46
+ * · 什么条件下删:**下一个 major**,且届时三方都已改调(或各自明确表态不改并自己认账),且那时**仍**
47
+ * 是零消费 —— 那天「删」的理由才成立。今天不成立:今天的零是手抄造出来的,不是这面没人要。
27
48
  */
28
49
  import { z } from "zod";
29
50
  // ── endpoint paths + protocol constants (shipped values) ─────────────────────────────────────────────────
@@ -196,7 +196,7 @@ export interface EntrySalvageResult {
196
196
  * 一条粘错位置的凭据,所以路径里的键段过 {@link redactSecrets} 才出门(不变量①:secret 值永不随错误外流,
197
197
  * 与 parseDomain 的 R13/R14 同一条);② 整段遍历包 try/catch —— 读字段、枚举 Record 都会触发访问器,
198
198
  * 抛出来的文案可能带 secret。抢救是**尽力而为**的:抢不动就当没抢救(返回原引用),让既有的整域档位去裁,
199
- * 绝不把解析拖下水、更不回显它的错误(与同文件 `scanDropped` 逐字同纪律)。
199
+ * 绝不把解析拖下水、更不回显它的错误(与同文件 {@link droppedKeys} 逐字同纪律)。
200
200
  *
201
201
  * 边界(如实登记):① 只看**顶层**字段(条目集合就在顶层,嵌套的坏形属于条目自身的问题);② raw 不是
202
202
  * 纯 JSON 对象、字段值与 schema 形状对不上(schema 说数组、文档给对象)⇒ 原样返回,交给整域档位裁;
@@ -227,7 +227,7 @@ export declare function statusRowsToPrune(rows: {
227
227
  * store upgrades with zero data migration and zero gate loss. An explicitly-set governance domain WINS
228
228
  * (its absence-of-a-key then means "center not managing", never "fall back to legacy runtime").
229
229
  *
230
- * SUNSET (1.11.0): removal is a **persistent-data** break — a store still carrying the 0.5 shape would lose
230
+ * SUNSET (2.0.0): removal is a **persistent-data** break — a store still carrying the 0.5 shape would lose
231
231
  * its guardrails silently, so this arm cannot simply be deleted at a minor. It goes at the next MAJOR, with
232
232
  * the standard persistent-data treatment (a loud refusal naming the domain + a one-time migration on the
233
233
  * writer side), and the closing condition is observable rather than a promise: `buildEffective` now emits
@@ -273,7 +273,7 @@ export type EffectiveReadWarning = {
273
273
  }
274
274
  /** 0.5 期的 runtime 文档里还带着治理三闸,而 governance 域从未被设过 —— 读侧抬升腿真生效了。
275
275
  * `keys` = 这次抬起来的那几个键。**照旧生效,只是不再无声**:兼容窗要能关掉,前提是有人知道它还开着。
276
- * 1.11.0 起。 */
276
+ * 2.0.0 起。 */
277
277
  | {
278
278
  domain: "governance";
279
279
  kind: "legacy-governance-lifted";
@@ -282,7 +282,7 @@ export type EffectiveReadWarning = {
282
282
  /** 一条**条目**没通过它自己的 entry schema,被单独丢掉了(域内其余条目照常生效)。`path` = 段坐标
283
283
  * (`["models",3]` / `["roles","summarize"]`),与 `unknown-keys-dropped` 的坐标形同一套。逐条一发 —— 载荷是
284
284
  * O(域里的条目数),与被读文档同阶,不放大,故不设条数上限(有上限的是**单条**告警里的键列表)。
285
- * 1.11.0 起;只可能来自 {@link DOMAIN_READ_FALLBACK} 判 `"default"` 的域,gate 域坏形仍然 throw。 */
285
+ * 2.0.0 起;只可能来自 {@link DOMAIN_READ_FALLBACK} 判 `"default"` 的域,gate 域坏形仍然 throw。 */
286
286
  | {
287
287
  domain: DomainName;
288
288
  kind: "entry-dropped";
@@ -296,6 +296,46 @@ export type EffectiveReadWarning = {
296
296
  domain?: DomainName;
297
297
  why: "not-a-domain" | "non-portable-domain";
298
298
  };
299
+ /** {@link EffectiveReadWarning} 的 `kind` **闭集**(运行期值)。一致性 kit 的覆盖律按它计数,消费方要遍历
300
+ * 「所有告警种类」也用它 —— 属主在告警联合自己的模块里,`./conformance` 只再导出(4.0.0 前它住在 kit 里,
301
+ * 那边是联合的下游,却握着联合的闭集)。
302
+ *
303
+ * **这张表由谁咬住**(实测,2026-09-22,别照 `satisfies` 的样子推):`satisfies` 只咬「多一个联合里**没有**
304
+ * 的词」;**少登记一个** kind 它咬不住(少一员的数组照样可赋值)。真正咬「新 kind 必须被处置」的是三处
305
+ * `never` 穷举臂 —— {@link projectWarning}、{@link rebaseWarningPaths}、kit 的 `normalizeProjection`
306
+ * (给联合加一条新臂,实测这三处各红一条,`satisfies` 那行零诊断);而「新 kind 三处都处置了、却忘了登记
307
+ * 进本表」这半条缝,由 `test/warning-projection-derived-4.0.0.test.ts` 的闭集格看着(本表 == 从 AST 解出的
308
+ * 臂集,逐字同集同序)。 */
309
+ export declare const WARNING_KINDS: readonly ["hosts-grandfathered", "domain-defaulted", "unknown-keys-dropped", "unknown-keys-carried", "legacy-governance-lifted", "entry-dropped", "unread-config-file"];
310
+ export type WarningKind = (typeof WARNING_KINDS)[number];
311
+ /** 逐臂剥字段的**分配式**映射:`W` 是裸类型参数,条件类型因此对联合的每一员分别作用(而不是先塌成交集)。 */
312
+ type PerArmOmit<W, K extends string> = W extends unknown ? Omit<W, K> : never;
313
+ /**
314
+ * 一条告警的**投影** = 该臂**减掉 `error`**,逐臂**机器派生**(4.0.0 BREAKING:属主从
315
+ * `./conformance` 搬回这里,形从手写字段表改成派生)。
316
+ *
317
+ * 为什么投影要剥掉 `error`:它带的是 zod 的错误对象/文案,可经脱敏、可随 zod 小版本改字 —— 比文案就是比
318
+ * 实现。`error` 因此永不进比对(但也不是就这么放过去了:kit 的 `findSecretLeaks` 先断言它不含语料自带的
319
+ * 秘密清单)。
320
+ *
321
+ * 为什么必须派生:4.0.0 之前这是一张**手写**的字段表(住在 `src/conformance/warnings.ts`),与本联合之间
322
+ * 没有任何字段级约束 ⇒ 给已有 kind 加一个字段,投影不动、`projectWarning` 不动、跨仓一致性门照绿,新字段
323
+ * 就这样一路穿过 kit,两边比的是一份少一个字段的告警(而都显示全绿)。派生之后,加字段 = `projectWarning`
324
+ * 那张表立刻编译红(门③ 用逐臂变异证过:改前段内诊断恒 0,改后每臂 ≥1)。
325
+ *
326
+ * 分配律是这里的承重位:直接写 `Omit<EffectiveReadWarning, "error">` 会先把联合塌成交集再剥,`keys` /
327
+ * `path` / `file` 这些**只属于某几臂**的字段会整片消失 —— 所以走 {@link PerArmOmit}(裸类型参数的条件
328
+ * 类型对联合**逐员**作用)。
329
+ */
330
+ export type WarningProjection = PerArmOmit<EffectiveReadWarning, "error">;
331
+ /**
332
+ * 一条告警 → 它的投影。**闭集穷举**:新 kind 不在这里表态就编译红(#157 的闭集纪律);而字段级的对齐由
333
+ * {@link WarningProjection} 的派生保证 —— 少铸一个字段,这里就是编译红。
334
+ *
335
+ * `truncatedFrom` 缺席时**不铸这个键**(不是铸 `undefined`)—— 规范化会把 `undefined` 值剔掉,两种写法
336
+ * 最终同形,但不铸更诚实:「没有截断」与「截断了 0 条」不该在中间形上分成两件事。
337
+ */
338
+ export declare function projectWarning(w: EffectiveReadWarning): WarningProjection;
299
339
  /**
300
340
  * 一条告警里的**段坐标**({@link ConfigPath})从「抢救压缩后的坐标」回写成「原文档的坐标」—— 纯函数,
301
341
  * {@link EntryKeptIndex} 是它唯一的坐标账。
@@ -305,7 +345,7 @@ export type EffectiveReadWarning = {
305
345
  * `models.1` 指的是压缩后的第 1 条。运维照着这条告警去改原文档的第 1 条 —— 改的是别人的行,而且改完
306
346
  * 告警还在(他以为改对了)。**告警必须说运维看得见的那份文档里的坐标**。
307
347
  *
308
- * 为什么回写在这里、而不是让扫描函数认识原文档:给 `scanDropped` / `scanCarried` 加一个「原始 raw」
348
+ * 为什么回写在这里、而不是让扫描函数认识原文档:给 {@link droppedKeys} / {@link carriedUnknownKeys} 加一个「原始 raw」
309
349
  * 第二入参,等于让扫描同时持有两套坐标,每条新告警都要自己决定用哪套。回写是**告警出门前**的一次
310
350
  * 坐标换算,与诊断本身正交 —— 一个铸点,所有臂一次表态。
311
351
  *
@@ -358,6 +398,21 @@ export declare const EXTENSION_NAMESPACES: Partial<Record<DomainName, ReadonlyAr
358
398
  * 本身——键名里可以含 `.`(`semaX.foo` 是一个键不是两层),段形让这件事不必再靠文法猜;前缀契约就是前缀
359
399
  * 契约(`semaGPT_5` / `semaX-v2` 都是成员,后缀不设未声明的文法)。 */
360
400
  export declare function isReservedExtensionKey(domain: DomainName, parent: ConfigPath, key: string): boolean;
401
+ /**
402
+ * 一次未知键扫描的结果:`keys` 有界(≤ {@link DROP_SCAN_MAX_KEYS}),`total` 是**截断前**的真实未知键数
403
+ * (超界仍数完 —— 扫描本就 O(文档键数),上限只界**告警载荷**不界扫描);`total > keys.length` ⇔ 列表被截断。
404
+ *
405
+ * **4.0.0(BREAKING)**:{@link droppedKeys} / {@link carriedUnknownKeys} 从「只回 `keys` 数组」改成回本形。
406
+ * 成案:上界与真实总数此前都是私有的,于是一个自装配读腿的消费方(不走 {@link parseDomainLoud} 的
407
+ * `onWarning`、自己拿这两只函数做诊断)在**结构上**铸不出 `truncatedFrom` —— 拿到 64 条,既不知道是不是刚好
408
+ * 64 条,也没有第二个公开面可以问「一共多少」。而本包自己的告警面从 1.5.0 起就带着 `truncatedFrom`:
409
+ * 同一件事在两个公开面上一个说、一个不说。上界 {@link DROP_SCAN_MAX_KEYS} 仍然**私有** —— 它是告警载荷的
410
+ * 上界,不是契约(改它不该让任何消费方红);消费方要的是「有没有截断、截断前多少条」,那就是 `total`。
411
+ */
412
+ export interface UnknownKeyScan {
413
+ keys: ConfigPath[];
414
+ total: number;
415
+ }
361
416
  /**
362
417
  * 一份域文档里被域 schema **丢掉的键**(zod 对象默认 `"strip"`),段坐标形({@link ConfigPath})。
363
418
  *
@@ -373,20 +428,29 @@ export declare function isReservedExtensionKey(domain: DomainName, parent: Confi
373
428
  * `runtime` 域的治理三件({@link GOVERNANCE_MIRROR_KEYS})**不算被丢**:0.5 期 runtime 文档携带它们是
374
429
  * 成文的抬升位(本文件的 governance 抬升函数会把它们抬进 governance 域),报出来是噪声。豁免只在顶层。
375
430
  *
431
+ * 返回 {@link UnknownKeyScan}:`keys` 是**有界**的点名列表,`total` 是截断前的真实总数(4.0.0 BREAKING,
432
+ * 此前只回 `keys` —— 见 {@link UnknownKeyScan} 的成案)。本包自己的告警(`unknown-keys-dropped`)也是拿这两个
433
+ * 数拼出来的,**一个铸点**:消费方自铸的 `truncatedFrom` 与告警里那个恒等。
434
+ *
376
435
  * 边界(如实登记):① 深度上限 {@link DROP_SCAN_MAX_DEPTH}、键数上限 {@link DROP_SCAN_MAX_KEYS}(告警
377
- * 载荷有界,超出即少报);② 数组长度不同就整段跳过(元素级裁剪由各自的 clamp 告警负责);③ 只比对纯
378
- * JSON 对象——raw 若是类实例/Proxy 这类形状不扫;④ 读 raw 的值可能触发访问器,整段包 try/catch 且**绝不**
379
- * 让其错误外泄(错误文案可能带 secret,与 parseDomain 的 R13 同纪律),扫描失败即当作"没看见",不影响解析。
436
+ * 载荷有界,超出即少报**点名**——`total` 不少报);② 数组长度不同就整段跳过(元素级裁剪由各自的 clamp 告警
437
+ * 负责);③ 只比对纯 JSON 对象——raw 若是类实例/Proxy 这类形状不扫;④ 读 raw 的值可能触发访问器,整段包
438
+ * try/catch 且**绝不**让其错误外泄(错误文案可能带 secret,与 parseDomain 的 R13 同纪律),扫描失败即当作
439
+ * "没看见"(`{ keys: [], total: 0 }`),不影响解析。
380
440
  */
381
- export declare function droppedKeys(domain: DomainName, raw: unknown, parsed: unknown): ConfigPath[];
441
+ export declare function droppedKeys(domain: DomainName, raw: unknown, parsed: unknown): UnknownKeyScan;
382
442
  /**
383
443
  * 一份已解析的 open-world 域文档里,**本包不认识但已承运**的键(段坐标)。非 open-world 域恒空
384
444
  * (它们的未知键走 {@link droppedKeys} 那条更重的「被丢了」告警)。
385
445
  *
446
+ * 返回 {@link UnknownKeyScan}(4.0.0 BREAKING,与 {@link droppedKeys} 同形同律:`keys` 有界、`total` 是
447
+ * 截断前的真实总数)。非 open-world 域返回的也是这个形(`{ keys: [], total: 0 }`),不是别的东西 ——
448
+ * 「这个域没有承运未知键」与「这个域不参与承运」在消费方那里是同一个处置。
449
+ *
386
450
  * 边界(如实):只看 {@link OPEN_WORLD_KNOWN_KEYS} 登记了的层级 —— 未登记层级(如某个未知键自己的
387
451
  * 子树)不再下探,那本就是消费方自定义的内容,逐层猜没有意义。
388
452
  */
389
- export declare function carriedUnknownKeys(domain: DomainName, parsed: unknown): ConfigPath[];
453
+ export declare function carriedUnknownKeys(domain: DomainName, parsed: unknown): UnknownKeyScan;
390
454
  /**
391
455
  * 一域的 parse + **静默剥键/静默承运两条告警**(0.19.0)。`buildEffective` 与 `FileConfigStore.getDomain`
392
456
  * 共用这一个判据 —— 此前只有 effective 那条路径有门,而 `getDomain` 是 `exportBundle` 的读法
@@ -459,7 +523,7 @@ export interface GrandfatherHostsReadResult {
459
523
  * everything else (regex, types, duplicates) is left for parseDomain — this is a targeted grandfather, not a
460
524
  * lenient parser. PURE + idempotent; returns the ORIGINAL reference when nothing exceeds the bounds.
461
525
  *
462
- * SUNSET (1.11.0): same persistent-data class as the governance lift — it goes at the next MAJOR. This one was
526
+ * SUNSET (2.0.0): same persistent-data class as the governance lift — it goes at the next MAJOR. This one was
463
527
  * never silent (`hosts-grandfathered` fires whenever it clamps), so the closing condition is already
464
528
  * observable: a deployment that has not seen that warning across a full read cycle no longer needs the arm. */
465
529
  export declare function grandfatherHostsRead(raw: unknown): GrandfatherHostsReadResult;
@@ -488,3 +552,4 @@ export declare function readEffectiveWire(raw: unknown, onWarning?: (w: Effectiv
488
552
  * 组内一档未绑 → `undefined`(引擎档位层恒惰性,与「未配 tiers」逐字同义)。消费方(center /effective
489
553
  * 投影、service applyEffective 接线)都走这一个函数,不要各自解析。 */
490
554
  export declare function resolveActiveTiers(models: Pick<DomainConfig["models"], "tierGroups" | "activeTierGroup">): Record<string, string> | undefined;
555
+ export {};
@@ -123,7 +123,7 @@ export function parseDomain(domain, raw) {
123
123
  return r.data;
124
124
  throw sanitizeZodError(r.error); // recursive redaction (message/received/unionErrors) — no secret echo. R13/R14.
125
125
  }
126
- // ── 逐条容错律(1.11.0,S-144 按类修)───────────────────────────────────────────────────────────────
126
+ // ── 逐条容错律(2.0.0,S-144 按类修)───────────────────────────────────────────────────────────────
127
127
  //
128
128
  // 病:域解析整域 all-or-nothing —— 13 个模型里坏一条,整张目录退成 schema 默认 = 一条都不剩。缓解一直
129
129
  // 是**逐键**打补丁(把新键改成 `z.unknown()` 让它别拒),那是给每个新键加特判,规则只会越来越多;
@@ -207,7 +207,7 @@ function judgeEntry(schema, value) {
207
207
  * 一条粘错位置的凭据,所以路径里的键段过 {@link redactSecrets} 才出门(不变量①:secret 值永不随错误外流,
208
208
  * 与 parseDomain 的 R13/R14 同一条);② 整段遍历包 try/catch —— 读字段、枚举 Record 都会触发访问器,
209
209
  * 抛出来的文案可能带 secret。抢救是**尽力而为**的:抢不动就当没抢救(返回原引用),让既有的整域档位去裁,
210
- * 绝不把解析拖下水、更不回显它的错误(与同文件 `scanDropped` 逐字同纪律)。
210
+ * 绝不把解析拖下水、更不回显它的错误(与同文件 {@link droppedKeys} 逐字同纪律)。
211
211
  *
212
212
  * 边界(如实登记):① 只看**顶层**字段(条目集合就在顶层,嵌套的坏形属于条目自身的问题);② raw 不是
213
213
  * 纯 JSON 对象、字段值与 schema 形状对不上(schema 说数组、文档给对象)⇒ 原样返回,交给整域档位裁;
@@ -306,7 +306,7 @@ export function statusRowsToPrune(rows, opts) {
306
306
  * store upgrades with zero data migration and zero gate loss. An explicitly-set governance domain WINS
307
307
  * (its absence-of-a-key then means "center not managing", never "fall back to legacy runtime").
308
308
  *
309
- * SUNSET (1.11.0): removal is a **persistent-data** break — a store still carrying the 0.5 shape would lose
309
+ * SUNSET (2.0.0): removal is a **persistent-data** break — a store still carrying the 0.5 shape would lose
310
310
  * its guardrails silently, so this arm cannot simply be deleted at a minor. It goes at the next MAJOR, with
311
311
  * the standard persistent-data treatment (a loud refusal naming the domain + a one-time migration on the
312
312
  * writer side), and the closing condition is observable rather than a promise: `buildEffective` now emits
@@ -321,6 +321,58 @@ export function legacyGovernanceFromRuntime(rawRuntime) {
321
321
  lifted[k] = r[k];
322
322
  return Object.keys(lifted).length ? lifted : undefined;
323
323
  }
324
+ /** {@link EffectiveReadWarning} 的 `kind` **闭集**(运行期值)。一致性 kit 的覆盖律按它计数,消费方要遍历
325
+ * 「所有告警种类」也用它 —— 属主在告警联合自己的模块里,`./conformance` 只再导出(4.0.0 前它住在 kit 里,
326
+ * 那边是联合的下游,却握着联合的闭集)。
327
+ *
328
+ * **这张表由谁咬住**(实测,2026-09-22,别照 `satisfies` 的样子推):`satisfies` 只咬「多一个联合里**没有**
329
+ * 的词」;**少登记一个** kind 它咬不住(少一员的数组照样可赋值)。真正咬「新 kind 必须被处置」的是三处
330
+ * `never` 穷举臂 —— {@link projectWarning}、{@link rebaseWarningPaths}、kit 的 `normalizeProjection`
331
+ * (给联合加一条新臂,实测这三处各红一条,`satisfies` 那行零诊断);而「新 kind 三处都处置了、却忘了登记
332
+ * 进本表」这半条缝,由 `test/warning-projection-derived-4.0.0.test.ts` 的闭集格看着(本表 == 从 AST 解出的
333
+ * 臂集,逐字同集同序)。 */
334
+ export const WARNING_KINDS = [
335
+ "hosts-grandfathered",
336
+ "domain-defaulted",
337
+ "unknown-keys-dropped",
338
+ "unknown-keys-carried",
339
+ "legacy-governance-lifted",
340
+ "entry-dropped",
341
+ "unread-config-file",
342
+ ];
343
+ /**
344
+ * 一条告警 → 它的投影。**闭集穷举**:新 kind 不在这里表态就编译红(#157 的闭集纪律);而字段级的对齐由
345
+ * {@link WarningProjection} 的派生保证 —— 少铸一个字段,这里就是编译红。
346
+ *
347
+ * `truncatedFrom` 缺席时**不铸这个键**(不是铸 `undefined`)—— 规范化会把 `undefined` 值剔掉,两种写法
348
+ * 最终同形,但不铸更诚实:「没有截断」与「截断了 0 条」不该在中间形上分成两件事。
349
+ */
350
+ export function projectWarning(w) {
351
+ switch (w.kind) {
352
+ case "hosts-grandfathered":
353
+ return { kind: w.kind, domain: w.domain, droppedNames: w.droppedNames, ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
354
+ case "domain-defaulted":
355
+ return { kind: w.kind, domain: w.domain };
356
+ // 两臂字段相同但**不合并**:合并后 `kind` 塌成两员的联合,TS 就再也认不出它属于投影联合的哪一员
357
+ // (真编译红过一次)。逐 kind 一臂,是这张表本来的样子。
358
+ case "unknown-keys-dropped":
359
+ return { kind: w.kind, domain: w.domain, keys: w.keys, ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
360
+ case "unknown-keys-carried":
361
+ return { kind: w.kind, domain: w.domain, keys: w.keys, ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
362
+ case "legacy-governance-lifted":
363
+ return { kind: w.kind, domain: w.domain, keys: w.keys };
364
+ case "entry-dropped":
365
+ return { kind: w.kind, domain: w.domain, path: w.path };
366
+ case "unread-config-file":
367
+ return { kind: w.kind, file: w.file, ...(w.domain === undefined ? {} : { domain: w.domain }), why: w.why };
368
+ default: {
369
+ const unhandled = w;
370
+ // 运行期到不了(本联合由本模块铸造);到了就让它红 —— 一条不认识的告警被静默投影成空,
371
+ // 正是「门看起来全绿」的那种假绿。
372
+ throw new Error(`settings-schema: unknown EffectiveReadWarning kind — ${JSON.stringify(unhandled)}`);
373
+ }
374
+ }
375
+ }
324
376
  /**
325
377
  * 一条告警里的**段坐标**({@link ConfigPath})从「抢救压缩后的坐标」回写成「原文档的坐标」—— 纯函数,
326
378
  * {@link EntryKeptIndex} 是它唯一的坐标账。
@@ -330,7 +382,7 @@ export function legacyGovernanceFromRuntime(rawRuntime) {
330
382
  * `models.1` 指的是压缩后的第 1 条。运维照着这条告警去改原文档的第 1 条 —— 改的是别人的行,而且改完
331
383
  * 告警还在(他以为改对了)。**告警必须说运维看得见的那份文档里的坐标**。
332
384
  *
333
- * 为什么回写在这里、而不是让扫描函数认识原文档:给 `scanDropped` / `scanCarried` 加一个「原始 raw」
385
+ * 为什么回写在这里、而不是让扫描函数认识原文档:给 {@link droppedKeys} / {@link carriedUnknownKeys} 加一个「原始 raw」
334
386
  * 第二入参,等于让扫描同时持有两套坐标,每条新告警都要自己决定用哪套。回写是**告警出门前**的一次
335
387
  * 坐标换算,与诊断本身正交 —— 一个铸点,所有臂一次表态。
336
388
  *
@@ -481,16 +533,6 @@ function collectDropped(domain, raw, parsed, prefix, depth, spared, acc) {
481
533
  collectDropped(domain, raw[k], parsed[k], [...prefix, k], depth + 1, spared, acc);
482
534
  }
483
535
  }
484
- function scanDropped(domain, raw, parsed) {
485
- const acc = { keys: [], total: 0 };
486
- try {
487
- collectDropped(domain, raw, parsed, [], 0, domain === "runtime" ? GOVERNANCE_MIRROR_KEYS : [], acc);
488
- }
489
- catch {
490
- return { keys: [], total: 0 }; // 反射/访问器抛错:告警是尽力而为的审计线,绝不能把解析拖下水,更不能回显其错误文案
491
- }
492
- return acc;
493
- }
494
536
  /**
495
537
  * 一份域文档里被域 schema **丢掉的键**(zod 对象默认 `"strip"`),段坐标形({@link ConfigPath})。
496
538
  *
@@ -506,13 +548,25 @@ function scanDropped(domain, raw, parsed) {
506
548
  * `runtime` 域的治理三件({@link GOVERNANCE_MIRROR_KEYS})**不算被丢**:0.5 期 runtime 文档携带它们是
507
549
  * 成文的抬升位(本文件的 governance 抬升函数会把它们抬进 governance 域),报出来是噪声。豁免只在顶层。
508
550
  *
551
+ * 返回 {@link UnknownKeyScan}:`keys` 是**有界**的点名列表,`total` 是截断前的真实总数(4.0.0 BREAKING,
552
+ * 此前只回 `keys` —— 见 {@link UnknownKeyScan} 的成案)。本包自己的告警(`unknown-keys-dropped`)也是拿这两个
553
+ * 数拼出来的,**一个铸点**:消费方自铸的 `truncatedFrom` 与告警里那个恒等。
554
+ *
509
555
  * 边界(如实登记):① 深度上限 {@link DROP_SCAN_MAX_DEPTH}、键数上限 {@link DROP_SCAN_MAX_KEYS}(告警
510
- * 载荷有界,超出即少报);② 数组长度不同就整段跳过(元素级裁剪由各自的 clamp 告警负责);③ 只比对纯
511
- * JSON 对象——raw 若是类实例/Proxy 这类形状不扫;④ 读 raw 的值可能触发访问器,整段包 try/catch 且**绝不**
512
- * 让其错误外泄(错误文案可能带 secret,与 parseDomain 的 R13 同纪律),扫描失败即当作"没看见",不影响解析。
556
+ * 载荷有界,超出即少报**点名**——`total` 不少报);② 数组长度不同就整段跳过(元素级裁剪由各自的 clamp 告警
557
+ * 负责);③ 只比对纯 JSON 对象——raw 若是类实例/Proxy 这类形状不扫;④ 读 raw 的值可能触发访问器,整段包
558
+ * try/catch 且**绝不**让其错误外泄(错误文案可能带 secret,与 parseDomain 的 R13 同纪律),扫描失败即当作
559
+ * "没看见"(`{ keys: [], total: 0 }`),不影响解析。
513
560
  */
514
561
  export function droppedKeys(domain, raw, parsed) {
515
- return scanDropped(domain, raw, parsed).keys;
562
+ const acc = { keys: [], total: 0 };
563
+ try {
564
+ collectDropped(domain, raw, parsed, [], 0, domain === "runtime" ? GOVERNANCE_MIRROR_KEYS : [], acc);
565
+ }
566
+ catch {
567
+ return { keys: [], total: 0 }; // 反射/访问器抛错:告警是尽力而为的审计线,绝不能把解析拖下水,更不能回显其错误文案
568
+ }
569
+ return acc;
516
570
  }
517
571
  /**
518
572
  * open-world 域(`.passthrough()`)的**已知键表**:段坐标 → 该层已知键(`[]` = 顶层)。键集从 schema 的
@@ -535,13 +589,14 @@ const OPEN_WORLD_KNOWN_KEYS = {
535
589
  * 一份已解析的 open-world 域文档里,**本包不认识但已承运**的键(段坐标)。非 open-world 域恒空
536
590
  * (它们的未知键走 {@link droppedKeys} 那条更重的「被丢了」告警)。
537
591
  *
592
+ * 返回 {@link UnknownKeyScan}(4.0.0 BREAKING,与 {@link droppedKeys} 同形同律:`keys` 有界、`total` 是
593
+ * 截断前的真实总数)。非 open-world 域返回的也是这个形(`{ keys: [], total: 0 }`),不是别的东西 ——
594
+ * 「这个域没有承运未知键」与「这个域不参与承运」在消费方那里是同一个处置。
595
+ *
538
596
  * 边界(如实):只看 {@link OPEN_WORLD_KNOWN_KEYS} 登记了的层级 —— 未登记层级(如某个未知键自己的
539
597
  * 子树)不再下探,那本就是消费方自定义的内容,逐层猜没有意义。
540
598
  */
541
599
  export function carriedUnknownKeys(domain, parsed) {
542
- return scanCarried(domain, parsed).keys;
543
- }
544
- function scanCarried(domain, parsed) {
545
600
  const table = OPEN_WORLD_KNOWN_KEYS[domain];
546
601
  const acc = { keys: [], total: 0 };
547
602
  if (table === undefined)
@@ -571,10 +626,10 @@ function scanCarried(domain, parsed) {
571
626
  export function parseDomainLoud(domain, raw, onWarning) {
572
627
  const parsed = parseDomain(domain, raw);
573
628
  if (onWarning) {
574
- const dropped = scanDropped(domain, raw, parsed);
629
+ const dropped = droppedKeys(domain, raw, parsed);
575
630
  if (dropped.keys.length > 0)
576
631
  onWarning({ domain, kind: "unknown-keys-dropped", keys: dropped.keys, ...(dropped.total > dropped.keys.length ? { truncatedFrom: dropped.total } : {}) });
577
- const carried = scanCarried(domain, parsed);
632
+ const carried = carriedUnknownKeys(domain, parsed);
578
633
  if (carried.keys.length > 0)
579
634
  onWarning({ domain, kind: "unknown-keys-carried", keys: carried.keys, ...(carried.total > carried.keys.length ? { truncatedFrom: carried.total } : {}) });
580
635
  }
@@ -637,7 +692,7 @@ export const DOMAIN_READ_FALLBACK = {
637
692
  * everything else (regex, types, duplicates) is left for parseDomain — this is a targeted grandfather, not a
638
693
  * lenient parser. PURE + idempotent; returns the ORIGINAL reference when nothing exceeds the bounds.
639
694
  *
640
- * SUNSET (1.11.0): same persistent-data class as the governance lift — it goes at the next MAJOR. This one was
695
+ * SUNSET (2.0.0): same persistent-data class as the governance lift — it goes at the next MAJOR. This one was
641
696
  * never silent (`hosts-grandfathered` fires whenever it clamps), so the closing condition is already
642
697
  * observable: a deployment that has not seen that warning across a full read cycle no longer needs the arm. */
643
698
  export function grandfatherHostsRead(raw) {
@@ -697,7 +752,7 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
697
752
  catch (e) {
698
753
  if (DOMAIN_READ_FALLBACK[domain] === "throw")
699
754
  throw e;
700
- // 逐条容错(1.11.0)在整域降级**之前**试一次:只丢掉真正坏掉的那几条条目,同域好兄弟活下来。
755
+ // 逐条容错(2.0.0)在整域降级**之前**试一次:只丢掉真正坏掉的那几条条目,同域好兄弟活下来。
701
756
  // 告警先缓存再决定 —— 一个域只给一个结论:救活了就逐条报 `entry-dropped`,没救活就只报
702
757
  // `domain-defaulted`(那一域的条目反正一条都不剩,再讲「丢了第 3 条」只会误导)。
703
758
  const salvage = salvageEntries(DOMAIN_SCHEMAS[domain], raw);
@@ -728,7 +783,7 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
728
783
  let governanceSource = governanceRaw;
729
784
  if (governanceRaw === undefined) {
730
785
  const lifted = legacyGovernanceFromRuntime(getRaw("runtime"));
731
- // 兼容臂真生效时出声(1.11.0):行为一字不变,但窗要能关就得有人看得见它还开着。
786
+ // 兼容臂真生效时出声(2.0.0):行为一字不变,但窗要能关就得有人看得见它还开着。
732
787
  if (lifted !== undefined)
733
788
  onWarning?.({ domain: "governance", kind: "legacy-governance-lifted", keys: Object.keys(lifted).map((k) => [k]) });
734
789
  governanceSource = lifted;
@@ -6,11 +6,18 @@
6
6
  * `buildEffective` / `readEffectiveWire` / `loadLocalConfig` **就是**它的生产入口);②historical 模式要对
7
7
  * 一份旧版 dist 跑同样的三腿。
8
8
  *
9
+ * 4.0.0 起这句自陈**有牙**:入口落在契约包自己身上 ⇒ 造适配器那一刻就**拒**(抛,不是告警 —— 一条告警
10
+ * 在 CI 日志里活不过第二天,而这道门一旦被同义反复接进去,它报的全绿是**假绿**)。判别式单铸在
11
+ * `./entry-origin.js`,`runConformance` 的 release 门用的是同一只(手写的适配器字面量走不到工厂,
12
+ * 那条路只有门看得见)。两条腿的拒绝句也同源:说清你落在哪儿、合法的是哪儿。
13
+ *
9
14
  * 拒绝分层的判据(一条,不是一串特例):**ZodError 形的错 = `validate`**(某个域没通过它的 schema);
10
15
  * 其余按腿各自的默认层 —— wire 腿是 `envelope`(它在解析任何域之前先判信封),local-root 腿是 `parse`
11
16
  * (文件层先判:空文件 / 非 JSON / 非对象),domains 腿本来就只有校验这一层。
12
17
  */
13
18
  import { materializeLocalRoot } from "./run.js";
19
+ import { contractOwnedEntry, contractOwnedEntryWhy } from "./entry-origin.js";
20
+ import { LEG_NAMES } from "./types.js";
14
21
  /** ZodError 形(鸭子判,不用 `instanceof` —— historical 模式下旧版 dist 可能挂在另一份 zod 上)。 */
15
22
  const isZodLike = (e) => e !== null && typeof e === "object" && Array.isArray(e.issues);
16
23
  /** 内容哈希那一层拒绝的自述 —— `stableStringify` 的深度闸是**本包自己**抛的、也只有它会抛这一句;
@@ -21,7 +28,37 @@ const reject = (e, fallback) => {
21
28
  const stage = isZodLike(e) ? "validate" : diagnostics.startsWith(HASH_STAGE_MARK) ? "hash" : fallback;
22
29
  return { outcome: "rejected", stage, diagnostics };
23
30
  };
31
+ /**
32
+ * 入口的**运行期拒**(fail-closed):三条腿的入口一条都不许落在**这份正在跑的**契约包自己的构建产物里,
33
+ * 缺席也不行。判别式与拒绝句来自 `./entry-origin.js`,这里只负责「拒」这个动作 —— 以及**按形分派**:
34
+ * 工厂看不见 `mode`,所以它只拒在任何模式下都错的那一形(`own-build-output` = 拿自己比自己);
35
+ * `another-installed-copy` 在 `historical` 模式下是正当的(那个模式就是拿旧版包比行为,而旧版包常常正好
36
+ * 装在 `node_modules/<包名>/` 布局下)⇒ 留给看得见 mode 的 release 门点名,工厂不越权。
37
+ * 形是闭集,`default` 那支是**编译期**守卫:新加一形而不在这里表态 ⇒ `never` 赋值红。
38
+ */
39
+ function refuseContractOwnedEntries(entries) {
40
+ for (const leg of LEG_NAMES) {
41
+ const entry = entries[leg];
42
+ if (entry === undefined || typeof entry.module !== "string" || entry.module.length === 0) {
43
+ throw new Error(`conformance: leg \`${leg}\` 缺 entry.module —— 三条腿各自的**生产入口**坐标是必填的(来源律 §2d-2)`);
44
+ }
45
+ const owned = contractOwnedEntry(entry.module);
46
+ if (owned === undefined)
47
+ continue;
48
+ switch (owned.form) {
49
+ case "own-build-output":
50
+ throw new Error(`conformance: ${contractOwnedEntryWhy(leg, entry.module, owned)}`);
51
+ case "another-installed-copy":
52
+ continue; // 见上:historical 模式的正当形,归看得见 mode 的 release 门判
53
+ default: {
54
+ const unhandled = owned.form;
55
+ throw new Error(`conformance: 来源律判别式回了一个没人处置的形 ${JSON.stringify(unhandled)}`);
56
+ }
57
+ }
58
+ }
59
+ }
24
60
  export function createContractAdapter(opts) {
61
+ refuseContractOwnedEntries(opts.entries);
25
62
  const { mod } = opts;
26
63
  const run = async (input) => {
27
64
  const warnings = [];
@@ -0,0 +1,24 @@
1
+ /** 契约包在盘上的两种样子(闭集):**我自己**,与**另一份**。 */
2
+ export declare const CONTRACT_OWNED_FORMS: readonly ["own-build-output", "another-installed-copy"];
3
+ export type ContractOwnedForm = (typeof CONTRACT_OWNED_FORMS)[number];
4
+ /** 判别式命中:哪一形、命中的是哪一段路径(拒绝句要说得出「你落在哪儿」)。 */
5
+ export interface ContractOwnedEntry {
6
+ form: ContractOwnedForm;
7
+ /** 命中的那段路径前缀(逐字,给人看的坐标)。 */
8
+ anchor: string;
9
+ }
10
+ /**
11
+ * 这个入口坐标落在契约包自己身上吗?命中回形与坐标,没命中回 `undefined`。
12
+ *
13
+ * ⚠️ **两形的判定顺序是承重的**:消费方装好的那份 kit **自己就住在 `node_modules/<包名>/` 里**,它的
14
+ * `dist/` 同时命中两形。「是不是**我自己**」必须先判 —— 否则最该拒的那一形(消费方拿自己比自己)会被
15
+ * 判成「另一份」而被工厂放行。先 `dist` 后 `node_modules`,两形于是不相交:`another-installed-copy`
16
+ * 字面意思就是**另一份**装好的契约包(historical 模式的正当目标)。
17
+ *
18
+ * ⚠️ **判之前先归一**(两侧都归):`<包根>/./dist/x.js` 与 `<包根>//dist/x.js` 与一条指向 `<包根>/dist`
19
+ * 的软链,指的都是同一个文件。release 门会先 `realpath` 再判,但 `historical` 模式根本不跑那道门 ——
20
+ * 工厂这一侧是那条路上唯一的闸,所以归一得在**判别式里**做,两处执法才咬同一个判据。
21
+ */
22
+ export declare function contractOwnedEntry(modulePath: string): ContractOwnedEntry | undefined;
23
+ /** 拒绝句/违例句共用的那一段:你落在哪儿、合法的是哪儿。两处文案同源,消费方读到的是同一句话。 */
24
+ export declare function contractOwnedEntryWhy(leg: string, modulePath: string, owned: ContractOwnedEntry): string;