@sema-agent/settings-schema 10.1.0 → 11.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 (235) hide show
  1. package/CHANGELOG.md +419 -0
  2. package/README.md +141 -65
  3. package/bin/settings-schema.mjs +13 -6
  4. package/conformance/VERSION +1 -1
  5. package/conformance/baseline.json +3 -1
  6. package/conformance/cases/c01-entry-salvage-then-typo/expected.json +3 -1
  7. package/conformance/cases/c02-hosts-clamp-stacked/expected.json +12 -5
  8. package/conformance/cases/c02-hosts-clamp-stacked/meta.json +1 -1
  9. package/conformance/cases/c03-literal-dotted-top-key/expected.json +22 -2
  10. package/conformance/cases/c04-all-bad-collection-not-salvaged/expected.json +3 -1
  11. package/conformance/cases/c06-legacy-governance-lift/expected.json +11 -101
  12. package/conformance/cases/c06-legacy-governance-lift/meta.json +1 -1
  13. package/conformance/cases/c07-hooks-is-not-a-config-domain/expected.json +3 -1
  14. package/conformance/cases/c08-limits-unknown-carried-64/expected.json +203 -2
  15. package/conformance/cases/c09-limits-unknown-carried-65/expected.json +204 -2
  16. package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +3 -1
  17. package/conformance/cases/c11-unicode-unknown-keys/expected.json +20 -2
  18. package/conformance/cases/c12-prototype-name-top-keys/expected.json +3 -1
  19. package/conformance/cases/c13-models-field-null/expected.json +3 -1
  20. package/conformance/cases/c14-models-field-empty-array/expected.json +6 -1
  21. package/conformance/cases/c15-models-field-number/expected.json +3 -1
  22. package/conformance/cases/c16-deep-nesting-205/expected.json +14 -2
  23. package/conformance/cases/c17-empty-domain-file/expected.json +3 -1
  24. package/conformance/cases/c18-wire-envelope-not-an-object/expected.json +3 -1
  25. package/conformance/cases/c19-wire-envelope-no-version/expected.json +3 -1
  26. package/conformance/cases/c20-governance-absent-lifts/expected.json +11 -86
  27. package/conformance/cases/c20-governance-absent-lifts/meta.json +1 -1
  28. package/conformance/cases/c21-governance-empty-object-wins/expected.json +6 -5
  29. package/conformance/cases/c21-governance-empty-object-wins/input.json +1 -4
  30. package/conformance/cases/c21-governance-empty-object-wins/meta.json +1 -1
  31. package/conformance/cases/c23-mixed-validity-models/expected.json +3 -1
  32. package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +22 -2
  33. package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +6 -1
  34. package/conformance/cases/c26-hosts-array-1024/expected.json +3 -1
  35. package/conformance/cases/c27-hosts-array-1025/expected.json +6 -7175
  36. package/conformance/cases/c27-hosts-array-1025/meta.json +1 -1
  37. package/conformance/cases/c28-large-document-over-1mb/expected.json +14 -2
  38. package/conformance/cases/c29-salvage-then-domain-refine-fails/expected.json +3 -1
  39. package/conformance/cases/c30-version-triple-a-first/expected.json +6 -1
  40. package/conformance/cases/c31-version-triple-b-changed/expected.json +6 -1
  41. package/conformance/cases/c32-version-triple-a-restored/expected.json +6 -1
  42. package/conformance/cases/c33-limits-nested-carried-key/expected.json +18 -2
  43. package/conformance/cases/c34-skills-content-vs-manifest/expected.json +19 -3
  44. package/conformance/cases/c35-portable-domain-file-is-null/expected.json +3 -1
  45. package/conformance/cases/c36-skills-entry-without-content/expected.json +6 -2
  46. package/conformance/cases/c37-skills-field-null/expected.json +6 -2
  47. package/conformance/cases/c38-remote-exec-is-a-non-domain-file/expected.json +3 -1
  48. package/conformance/cases/c39-hidden-file-in-config-d/expected.json +3 -1
  49. package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +11 -4
  50. package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +9 -3
  51. package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +39 -2
  52. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +4 -1
  53. package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +3 -1
  54. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +19 -10
  55. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/meta.json +1 -1
  56. package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/expected.json +12 -5
  57. package/conformance/cases/c46-s811-host-name-url-userinfo-withheld/meta.json +9 -0
  58. package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/expected.json +23 -3
  59. package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/input.json +6 -0
  60. package/conformance/cases/c47-s811-roles-key-url-userinfo-withheld/meta.json +10 -0
  61. package/conformance/cases/c48-s935-model-headers-transported/expected.json +6 -1
  62. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +3 -1
  63. package/conformance/cases/c50-s784-lane-host-legal/expected.json +3 -1
  64. package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +3 -1
  65. package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +3 -1
  66. package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +3 -1
  67. package/conformance/cases/c54-s784-lane-adb-legal/expected.json +3 -1
  68. package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +3 -1
  69. package/conformance/cases/c56-s784-lane-device-legal/expected.json +3 -1
  70. package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +3 -1
  71. package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +3 -1
  72. package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +3 -1
  73. package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +3 -1
  74. package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +3 -1
  75. package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +3 -1
  76. package/conformance/cases/c63-s784-host-two-replicas/expected.json +3 -1
  77. package/conformance/cases/c64-s784-multitenant-local-store/expected.json +3 -1
  78. package/conformance/cases/c65-s784-pg-without-db/expected.json +3 -1
  79. package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +3 -1
  80. package/conformance/cases/c67-s784-retired-field-loud/expected.json +3 -1
  81. package/conformance/cases/c67-s784-retired-field-loud/meta.json +1 -1
  82. package/conformance/cases/c68-s784-device-local-store/expected.json +3 -1
  83. package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +3 -1
  84. package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +3 -1
  85. package/conformance/cases/c71-one-scanner-write-reads-display-forms/expected.json +119 -0
  86. package/conformance/cases/c71-one-scanner-write-reads-display-forms/input.json +24 -0
  87. package/conformance/cases/c71-one-scanner-write-reads-display-forms/meta.json +10 -0
  88. package/conformance/cases/c72-dangling-reference-write-seat-refuses/expected.json +111 -0
  89. package/conformance/cases/c72-dangling-reference-write-seat-refuses/input.json +26 -0
  90. package/conformance/cases/c72-dangling-reference-write-seat-refuses/meta.json +6 -0
  91. package/conformance/cases/cc01-mcp-toolfaces-non-object/expected.json +3 -1
  92. package/conformance/cases/cc02-mcp-source-key-is-transported/expected.json +3 -1
  93. package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +6 -1
  94. package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +11 -1
  95. package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +11 -1
  96. package/conformance/cases/cli03-credential-pasted-into-unknown-key/expected.json +16 -2
  97. package/conformance/cases/cli04-env-value-with-newline/expected.json +3 -1
  98. package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +6 -1
  99. package/conformance/cases/t01-absent-domain-key-not-null/expected.json +3 -1
  100. package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +29 -2
  101. package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +3 -1
  102. package/conformance/cases/z01-every-domain-empty-object/expected.json +8 -4
  103. package/conformance/cases/z01-every-domain-empty-object/input.json +0 -1
  104. package/conformance/cases/z01-every-domain-empty-object/meta.json +1 -1
  105. package/conformance/cases/z02-every-domain-unknown-key/expected.json +163 -23
  106. package/conformance/cases/z02-every-domain-unknown-key/input.json +0 -3
  107. package/conformance/cases/z02-every-domain-unknown-key/meta.json +1 -1
  108. package/dist/api/auth-bridge.d.ts +51 -181
  109. package/dist/api/auth-bridge.js +58 -33
  110. package/dist/api/auth.d.ts +37 -146
  111. package/dist/api/auth.js +16 -26
  112. package/dist/api/feedback.d.ts +87 -0
  113. package/dist/api/feedback.js +66 -0
  114. package/dist/api/pull-token.d.ts +32 -0
  115. package/dist/api/pull-token.js +88 -0
  116. package/dist/api/scopes.d.ts +82 -174
  117. package/dist/api/scopes.js +23 -18
  118. package/dist/bundle.d.ts +15 -7
  119. package/dist/bundle.js +28 -27
  120. package/dist/cli.d.ts +3 -1
  121. package/dist/cli.js +132 -81
  122. package/dist/config-d-file.d.ts +14 -0
  123. package/dist/config-d-file.js +67 -0
  124. package/dist/config-fns.d.ts +87 -186
  125. package/dist/config-fns.js +207 -262
  126. package/dist/conformance/contract-adapter.d.ts +9 -2
  127. package/dist/conformance/contract-adapter.js +15 -7
  128. package/dist/conformance/corpus.d.ts +3 -0
  129. package/dist/conformance/corpus.js +5 -0
  130. package/dist/conformance/derive.d.ts +3 -0
  131. package/dist/conformance/index.d.ts +2 -2
  132. package/dist/conformance/index.js +2 -2
  133. package/dist/conformance/run.js +67 -22
  134. package/dist/conformance/types.d.ts +31 -15
  135. package/dist/conformance/types.js +5 -0
  136. package/dist/conformance/warnings.d.ts +1 -1
  137. package/dist/conformance/warnings.js +1 -4
  138. package/dist/cross-domain.d.ts +66 -22
  139. package/dist/cross-domain.js +164 -93
  140. package/dist/cross-session.d.ts +29 -11
  141. package/dist/default-model.d.ts +43 -0
  142. package/dist/default-model.js +34 -0
  143. package/dist/execution-lane-index.d.ts +4 -8
  144. package/dist/execution-lane-index.js +4 -8
  145. package/dist/execution-lanes.d.ts +31 -8
  146. package/dist/execution-lanes.js +23 -5
  147. package/dist/file-edit.js +5 -2
  148. package/dist/file-store.d.ts +7 -6
  149. package/dist/file-store.js +37 -50
  150. package/dist/fleet.d.ts +364 -762
  151. package/dist/fleet.js +277 -224
  152. package/dist/hooks.d.ts +1151 -4737
  153. package/dist/hooks.js +59 -15
  154. package/dist/index.d.ts +10 -4
  155. package/dist/index.js +13 -7
  156. package/dist/integer.d.ts +11 -0
  157. package/dist/integer.js +11 -0
  158. package/dist/issue-path.d.ts +10 -0
  159. package/dist/issue-path.js +10 -0
  160. package/dist/loopback.d.ts +26 -0
  161. package/dist/loopback.js +54 -0
  162. package/dist/migration-receipt.d.ts +42 -0
  163. package/dist/migration-receipt.js +12 -0
  164. package/dist/one-time-migrations.d.ts +56 -0
  165. package/dist/one-time-migrations.js +819 -0
  166. package/dist/prompts-digest.d.ts +1 -1
  167. package/dist/prompts-digest.js +3 -2
  168. package/dist/reader.d.ts +2 -2
  169. package/dist/remote-exec.d.ts +45 -187
  170. package/dist/remote-exec.js +13 -7
  171. package/dist/resolve-roster.d.ts +18 -3
  172. package/dist/resolve-roster.js +28 -25
  173. package/dist/retired-envs.d.ts +24 -0
  174. package/dist/retired-envs.js +114 -0
  175. package/dist/retired-form.d.ts +31 -0
  176. package/dist/retired-form.js +28 -0
  177. package/dist/safety-merge-spec.d.ts +131 -191
  178. package/dist/safety-merge-spec.js +18 -14
  179. package/dist/scheduler-lifetime.d.ts +5 -0
  180. package/dist/scheduler-lifetime.js +7 -0
  181. package/dist/scheduler-store-migration.d.ts +14 -0
  182. package/dist/scheduler-store-migration.js +14 -0
  183. package/dist/scheduler-store-node.d.ts +24 -9
  184. package/dist/scheduler-store-node.js +25 -15
  185. package/dist/scheduler-store.d.ts +47 -14
  186. package/dist/scheduler-store.js +32 -61
  187. package/dist/secret-refs.d.ts +3 -3
  188. package/dist/secret-refs.js +1 -1
  189. package/dist/secret-scan.d.ts +14 -24
  190. package/dist/secret-scan.js +45 -23
  191. package/dist/sha256-core.d.ts +16 -0
  192. package/dist/sha256-core.js +138 -0
  193. package/dist/sha256.d.ts +6 -1
  194. package/dist/sha256.js +8 -98
  195. package/dist/skills-manifest.js +1 -1
  196. package/dist/types.d.ts +1501 -13536
  197. package/dist/types.js +494 -438
  198. package/dist/url-shape.d.ts +34 -0
  199. package/dist/url-shape.js +61 -0
  200. package/dist/web-admin-credential.d.ts +96 -0
  201. package/dist/{center-credential.js → web-admin-credential.js} +46 -43
  202. package/dist/web-admin-env.d.ts +33 -0
  203. package/dist/web-admin-env.js +33 -0
  204. package/dist/worker-row-verdict.d.ts +16 -0
  205. package/dist/worker-row-verdict.js +31 -0
  206. package/dist/workers-rows.d.ts +34 -0
  207. package/dist/workers-rows.js +42 -0
  208. package/dist/zod-unwrap.d.ts +11 -0
  209. package/dist/zod-unwrap.js +25 -0
  210. package/package.json +17 -9
  211. package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/meta.json +0 -6
  212. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/meta.json +0 -6
  213. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/expected.json +0 -49
  214. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/input.json +0 -26
  215. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/meta.json +0 -4
  216. package/conformance/migrate-workers/m02-g5-idempotent/expected.json +0 -38
  217. package/conformance/migrate-workers/m02-g5-idempotent/input.json +0 -33
  218. package/conformance/migrate-workers/m02-g5-idempotent/meta.json +0 -5
  219. package/conformance/migrate-workers/m03-g5-refused-no-lane/expected.json +0 -71
  220. package/conformance/migrate-workers/m03-g5-refused-no-lane/input.json +0 -42
  221. package/conformance/migrate-workers/m03-g5-refused-no-lane/meta.json +0 -4
  222. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/expected.json +0 -52
  223. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/input.json +0 -30
  224. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/meta.json +0 -4
  225. package/conformance/migrate-workers/m05-row-verdict-reserved-env/expected.json +0 -82
  226. package/conformance/migrate-workers/m05-row-verdict-reserved-env/input.json +0 -68
  227. package/conformance/migrate-workers/m05-row-verdict-reserved-env/meta.json +0 -4
  228. package/dist/center-credential.d.ts +0 -116
  229. package/dist/issue-text.d.ts +0 -10
  230. package/dist/issue-text.js +0 -8
  231. package/dist/migrate-workers.d.ts +0 -71
  232. package/dist/migrate-workers.js +0 -351
  233. package/dist/migrate.d.ts +0 -255
  234. package/dist/migrate.js +0 -542
  235. /package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/input.json +0 -0
@@ -2,8 +2,15 @@ import type { ConformanceAdapter, ConformanceProvenance, LegEntry, LegName } fro
2
2
  /** 一份契约模块(新旧版同形 —— 四条腿的签名自 0.19.0 起没动过)。 */
3
3
  export interface ContractModule {
4
4
  buildEffective(version: number, getRaw: (d: string) => unknown, updatedAt: string, onWarning?: (w: unknown) => void): unknown;
5
- /** 严写受理的生产入口:一个域名 + 一份原始文档 → 受理后的域文档,不受理就抛。 */
6
- parseDomain(domain: string, raw: unknown): unknown;
5
+ /** 严写受理第一道:一个域名 + 一份原始文档 → 受理后的域文档,不受理就抛;受理时的告警(剥掉 / 承运的未知键)交给 `onWarning`。 */
6
+ parseDomainLoud(domain: string, raw: unknown, onWarning?: (w: unknown) => void): unknown;
7
+ /** 严写受理第二道 = **那一只引用判官**:整份(受理后的)文档集 → 每一条解析不到的跨域引用(`path` 以引用者的域键起手)。
8
+ * 一份文档集只判一次(不逐域各问一遍 —— 那样同一条悬空引用会被正反两向各报一次)。 */
9
+ danglingRefs(docs: Record<string, unknown>): ReadonlyArray<{
10
+ path: ReadonlyArray<string | number>;
11
+ }> | Promise<ReadonlyArray<{
12
+ path: ReadonlyArray<string | number>;
13
+ }>>;
7
14
  readEffectiveWire(raw: unknown, onWarning?: (w: unknown) => void): unknown;
8
15
  loadLocalConfig(root: string, opts?: {
9
16
  onWarning?: (w: unknown) => void;
@@ -6,9 +6,11 @@
6
6
  * `buildEffective` / `parseDomain` / `readEffectiveWire` / `loadLocalConfig` **就是**它的生产入口);②historical 模式要对
7
7
  * 一份旧版 dist 跑同样的四腿。
8
8
  *
9
- * `strictWrite`(严写受理)= 文档集里**每一个**键各自过 `parseDomain`(写入席收下的那份);任何一份不受理 ⇒ 整次
10
- * `rejected{stage:"validate"}`(写是一个动作,不是逐份抢救);全部受理 ⇒ `effective` = 键 → 受理后的域文档(只含入参里的
11
- * 键,不补缺席域)。`parseDomain` 不铸告警,所以这条腿的告警面恒空。
9
+ * `strictWrite`(严写受理)= 写入席的两道:① 文档集里**每一个**键各自过 `parseDomainLoud`(受理的那份 + 受理时的告警:剥掉 /
10
+ * 承运的未知键);任何一份不受理 ⇒ 整次 `rejected{stage:"validate"}`(写是一个动作,不是逐份抢救)。② 对文档集里每个引用受检域
11
+ * 整份受理后的文档集过一次**那一只引用判官** `danglingRefs`(文档集里没有的域 = 空;正反两向一次判完);
12
+ * 有悬空引用 ⇒ `rejected{stage:"validate", code: REF_INTEGRITY_REJECT_CODE, refIssues}`。都过 ⇒ `effective` = 键 → 受理后的域文档
13
+ * (只含入参里的键,不补缺席域)+ ① 的告警。
12
14
  *
13
15
  * 4.0.0 起这句自陈**有牙**:入口落在契约包自己身上 ⇒ 造适配器那一刻就**拒**(抛,不是告警 —— 一条告警
14
16
  * 在 CI 日志里活不过第二天,而这道门一旦被同义反复接进去,它报的全绿是**假绿**)。判别式单铸在
@@ -21,7 +23,7 @@
21
23
  */
22
24
  import { materializeLocalRoot } from "./run.js";
23
25
  import { contractOwnedEntry, contractOwnedEntryWhy } from "./entry-origin.js";
24
- import { LEG_NAMES } from "./types.js";
26
+ import { LEG_NAMES, REF_INTEGRITY_REJECT_CODE } from "./types.js";
25
27
  /** ZodError 形(鸭子判,不用 `instanceof` —— historical 模式下旧版 dist 可能挂在另一份 zod 上)。 */
26
28
  const isZodLike = (e) => e !== null && typeof e === "object" && Array.isArray(e.issues);
27
29
  /** 内容哈希那一层拒绝的自述 —— `stableStringify` 的深度闸是**本包自己**抛的、也只有它会抛这一句;
@@ -76,15 +78,21 @@ export function createContractAdapter(opts) {
76
78
  catch (e) {
77
79
  return reject(e, "validate");
78
80
  }
79
- case "strictWrite":
81
+ case "strictWrite": {
82
+ let accepted;
80
83
  try {
81
84
  // fromEntries(不是逐键赋值):文档集的键是用户写的,`__proto__` 作键时逐键赋值会走原型 setter。
82
- const accepted = Object.fromEntries(Object.entries(input.docs).map(([d, raw]) => [d, mod.parseDomain(d, raw)]));
83
- return { outcome: "effective", effective: accepted, warnings: [], version: input.version, updatedAt: input.updatedAt };
85
+ accepted = Object.fromEntries(Object.entries(input.docs).map(([d, raw]) => [d, mod.parseDomainLoud(d, raw, onWarning)]));
84
86
  }
85
87
  catch (e) {
86
88
  return reject(e, "validate");
87
89
  }
90
+ const refIssues = (await mod.danglingRefs(accepted)).map((issue) => ({ path: [...issue.path] }));
91
+ if (refIssues.length > 0) {
92
+ return { outcome: "rejected", stage: "validate", code: REF_INTEGRITY_REJECT_CODE, diagnostics: `${refIssues.length} dangling cross-domain reference(s)`, refIssues };
93
+ }
94
+ return { outcome: "effective", effective: accepted, warnings: warnings, version: input.version, updatedAt: input.updatedAt };
95
+ }
88
96
  case "wire":
89
97
  try {
90
98
  const effective = mod.readEffectiveWire(input.payload, onWarning);
@@ -24,6 +24,9 @@ export type LegExpectationJson = {
24
24
  outcome: "rejected";
25
25
  stage: RejectStage;
26
26
  code?: string;
27
+ refIssues?: Array<{
28
+ path: Array<string | number>;
29
+ }>;
27
30
  why?: string;
28
31
  };
29
32
  export interface CaseInput {
@@ -55,6 +55,11 @@ function checkLegExpectation(v, where) {
55
55
  if (r.outcome === "rejected") {
56
56
  if (!REJECT_STAGES.includes(r.stage))
57
57
  throw new Error(`conformance corpus: ${where}.stage must be one of ${REJECT_STAGES.join("|")}`);
58
+ if (r.refIssues !== undefined) {
59
+ const ok = Array.isArray(r.refIssues) && r.refIssues.every((x) => isRecord(x) && Array.isArray(x.path) && x.path.every((seg) => typeof seg === "string" || typeof seg === "number"));
60
+ if (!ok)
61
+ throw new Error(`conformance corpus: ${where}.refIssues must be an array of { path: (string|number)[] }`);
62
+ }
58
63
  return r;
59
64
  }
60
65
  throw new Error(`conformance corpus: ${where} is none of {derive:true} | {applicable:false,why} | {outcome:"effective"} | {outcome:"rejected"}`);
@@ -60,6 +60,9 @@ export type LegExpectation = {
60
60
  outcome: "rejected";
61
61
  stage: RejectStage;
62
62
  code?: string;
63
+ refIssues?: ReadonlyArray<{
64
+ path: ReadonlyArray<string | number>;
65
+ }>;
63
66
  };
64
67
  /** T1 + T2 + T4:`domains` 期望 → `wire` 期望。 */
65
68
  export declare function deriveWireExpectation(source: Extract<LegExpectation, {
@@ -2,7 +2,7 @@
2
2
  * `@sema-agent/settings-schema/conformance` —— 跨仓一致性门的 kit。
3
3
  *
4
4
  * 一句话:契约仓自带**金样语料 + 比对器**;每个消费方把**自己的生产读腿**塞给 kit 跑一遍 ⇒ 同一份
5
- * settings 语料在 center / worker / 管理面 / 契约仓自身读出的 effective 逐字节一致、告警多重集一致。
5
+ * settings 语料在 web-admin / worker / 管理面 / 契约仓自身读出的 effective 逐字节一致、告警多重集一致。
6
6
  *
7
7
  * 三步接线(消费方侧):
8
8
  * 1. 造一个 {@link ConformanceAdapter}:`provenance` = 你**实际加载到**的那份契约包(package.json 路径、
@@ -20,4 +20,4 @@ export { loadCorpus, loadCase, kitIdentity, DEFAULT_CORPUS_DIR, PACKAGE_ROOT, ty
20
20
  export { DERIVATION_TRANSFORMS, LEG_EXPECTATION, NON_PORTABLE_DOMAINS, EPOCH_ISO, deriveWireInput, deriveLocalInput, deriveWireExpectation, deriveLocalExpectation, foreignKeysOf, toManifestDoc, type DerivationTransform, type LegExpectation, } from "./derive.js";
21
21
  export { canonicalize, deepEqual, diffJson, stableKey, type JsonPatchOp } from "./canonical.js";
22
22
  export { WARNING_KINDS, projectWarning, normalizeProjection, projectionKey, projectionValue, diffWarningMultiset, findSecretLeaks, type WarningKind, type WarningProjection, } from "./warnings.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";
23
+ export { LEDGER_REF_RULE, LEG_DECLINE_REASONS, REF_INTEGRITY_REJECT_CODE, type RefIssueCoordinate, 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";
@@ -2,7 +2,7 @@
2
2
  * `@sema-agent/settings-schema/conformance` —— 跨仓一致性门的 kit。
3
3
  *
4
4
  * 一句话:契约仓自带**金样语料 + 比对器**;每个消费方把**自己的生产读腿**塞给 kit 跑一遍 ⇒ 同一份
5
- * settings 语料在 center / worker / 管理面 / 契约仓自身读出的 effective 逐字节一致、告警多重集一致。
5
+ * settings 语料在 web-admin / worker / 管理面 / 契约仓自身读出的 effective 逐字节一致、告警多重集一致。
6
6
  *
7
7
  * 三步接线(消费方侧):
8
8
  * 1. 造一个 {@link ConformanceAdapter}:`provenance` = 你**实际加载到**的那份契约包(package.json 路径、
@@ -20,4 +20,4 @@ export { loadCorpus, loadCase, kitIdentity, DEFAULT_CORPUS_DIR, PACKAGE_ROOT } f
20
20
  export { DERIVATION_TRANSFORMS, LEG_EXPECTATION, NON_PORTABLE_DOMAINS, EPOCH_ISO, deriveWireInput, deriveLocalInput, deriveWireExpectation, deriveLocalExpectation, foreignKeysOf, toManifestDoc, } from "./derive.js";
21
21
  export { canonicalize, deepEqual, diffJson, stableKey } from "./canonical.js";
22
22
  export { WARNING_KINDS, projectWarning, normalizeProjection, projectionKey, projectionValue, diffWarningMultiset, findSecretLeaks, } from "./warnings.js";
23
- export { LEG_DECLINE_REASONS, LEG_NAMES, REJECT_STAGES, } from "./types.js";
23
+ export { LEDGER_REF_RULE, LEG_DECLINE_REASONS, REF_INTEGRITY_REJECT_CODE, LEG_NAMES, REJECT_STAGES, } from "./types.js";
@@ -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_DECLINE_REASONS, LEG_NAMES, } from "./types.js";
22
+ import { LEDGER_REF_RULE, 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
  /**
@@ -120,7 +120,15 @@ export function resolveLegExpectation(c, leg, baseline) {
120
120
  }
121
121
  if (spec.outcome === "effective")
122
122
  return { outcome: "effective", effective: spec.effective, warnings: spec.warnings, metadata: spec.metadata };
123
- return { outcome: "rejected", stage: spec.stage, ...(spec.code === undefined ? {} : { code: spec.code }) };
123
+ return { outcome: "rejected", stage: spec.stage, ...(spec.code === undefined ? {} : { code: spec.code }), ...(spec.refIssues === undefined ? {} : { refIssues: spec.refIssues }) };
124
+ }
125
+ /** 两袋悬空引用坐标相等吗(多重集;坐标按结构比 —— 与告警坐标同一只稳定键)。 */
126
+ function sameRefIssueBag(expected, actual) {
127
+ if (actual === undefined || actual.length !== expected.length)
128
+ return false;
129
+ const key = (x) => stableKey(x.path);
130
+ const a = actual.map(key).sort();
131
+ return expected.map(key).sort().every((k, i) => k === a[i]);
124
132
  }
125
133
  /** effective 里的元数据两键(独立断言,不进结构比对)。 */
126
134
  const METADATA_KEYS = ["version", "updatedAt"];
@@ -186,10 +194,29 @@ function outcomeVerdict(base, detail, shape, ledger, hits) {
186
194
  hits[i] += 1;
187
195
  return { ...base, status: "diff-ledgered", detail: `${detail} — 落在适配器的已知分歧账本里(结局形,债)`, diff, ledgerRefs: [ledger[i].ref] };
188
196
  }
197
+ /** 一条腿交回的**每一处文本**里的语料秘密:读出 ⇒ 告警面(`findSecretLeaks`,同一只判据);拒绝 ⇒ 诊断句。 */
198
+ function legLeaks(actual, secrets) {
199
+ switch (actual.outcome) {
200
+ case "effective":
201
+ return findSecretLeaks(actual.warnings, secrets);
202
+ case "rejected":
203
+ return secrets.filter((s) => s.length > 0 && actual.diagnostics.includes(s));
204
+ default: {
205
+ const unhandled = actual;
206
+ throw new Error(`conformance: unknown outcome ${JSON.stringify(unhandled)}`);
207
+ }
208
+ }
209
+ }
189
210
  /** 一格的比对。返回 status + detail(+ diff)。`hits` 是账本逐条的命中计数器 —— 由本函数就地累加
190
211
  * (比对与计数走同一遍遍历;再扫一遍就是第二份判定,两份判定的分歧正是本门要抓的那类病)。 */
191
212
  function judge(c, leg, expected, actual, normalizer, ledger, hits) {
192
213
  const base = { caseId: c.id, leg };
214
+ // ① 秘密面**先于一切**判:一条腿交回的文本(读出时的告警、拒绝时的诊断句)里带着语料埋的秘密,比读数对不对、结局形对不对都严重
215
+ // —— 不论结局形是否一致、账本是否盖得住,这一格都是 `diff`,且不可入账。
216
+ const leaks = legLeaks(actual, c.meta.secrets ?? []);
217
+ if (leaks.length > 0) {
218
+ return { ...base, status: "diff", detail: `the leg's output leaked ${leaks.length} corpus secret(s) — 告警 / 诊断不许比它诊断的东西更危险`, diff: { effective: [], warnings: { missing: [], extra: leaks }, metadata: [] } };
219
+ }
193
220
  if (expected.outcome === "rejected") {
194
221
  if (actual.outcome !== "rejected") {
195
222
  return outcomeVerdict(base, `expected a rejection at stage \`${expected.stage}\`, got an effective config`, { expected: "rejected", actual: "effective" }, ledger, hits);
@@ -200,16 +227,14 @@ function judge(c, leg, expected, actual, normalizer, ledger, hits) {
200
227
  if (expected.code !== undefined && actual.code !== expected.code) {
201
228
  return outcomeVerdict(base, `expected rejection code \`${expected.code}\`, got \`${String(actual.code)}\``, { expected: "rejected", actual: "rejected", stage: actual.stage }, ledger, hits);
202
229
  }
230
+ if (expected.refIssues !== undefined && !sameRefIssueBag(expected.refIssues, actual.refIssues)) {
231
+ return outcomeVerdict(base, `expected dangling references at ${JSON.stringify(expected.refIssues.map((x) => x.path))}, got ${JSON.stringify(actual.refIssues?.map((x) => x.path) ?? "none")}`, { expected: "rejected", actual: "rejected", stage: actual.stage }, ledger, hits);
232
+ }
203
233
  return { ...base, status: "expected-rejected", detail: `rejected at \`${actual.stage}\` as expected` };
204
234
  }
205
235
  if (actual.outcome === "rejected") {
206
236
  return outcomeVerdict(base, `expected an effective config, got a rejection at \`${actual.stage}\`: ${actual.diagnostics}`, { expected: "effective", actual: "rejected", stage: actual.stage }, ledger, hits);
207
237
  }
208
- // ① 秘密面先判:告警里带着语料埋的秘密,比读数对不对更严重。
209
- const leaks = findSecretLeaks(actual.warnings, c.meta.secrets ?? []);
210
- if (leaks.length > 0) {
211
- return { ...base, status: "diff", detail: `warning surface leaked ${leaks.length} corpus secret(s) — 告警不许比它诊断的东西更危险`, diff: { effective: [], warnings: { missing: [], extra: leaks }, metadata: [] } };
212
- }
213
238
  // ② effective(剥元数据)
214
239
  const split = splitMetadata(actual.effective);
215
240
  const expectedBody = canonicalize(expected.effective);
@@ -255,6 +280,13 @@ function judge(c, leg, expected, actual, normalizer, ledger, hits) {
255
280
  }
256
281
  return { ...base, status: "diff", detail, diff };
257
282
  }
283
+ /** `YYYY-MM-DD` 且是真实的日历日(UTC;`2026-02-30` 不是)。 */
284
+ function isCalendarDate(v) {
285
+ if (typeof v !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(v))
286
+ return false;
287
+ const d = new Date(`${v}T00:00:00Z`);
288
+ return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === v;
289
+ }
258
290
  function stanceOf(adapter, leg) {
259
291
  const legs = adapter.legs;
260
292
  const s = legs !== null && typeof legs === "object" ? legs[leg] : undefined;
@@ -312,26 +344,39 @@ async function releaseViolations(adapter, opts, corpus, kit) {
312
344
  }
313
345
  }
314
346
  // 账本条目的**形**:一条大包大揽的 `pathPrefix`(`/` 或空串)不是账本,是一个 `ok=true` 开关;
315
- // 没有 `ref` / `flipWhen` 的一条登记没法被人查、也没法到期翻转(棘轮靠这两个字段才立得住)。
316
- for (const e of adapter.knownDivergences ?? []) {
317
- if (e.ref.trim().length === 0)
318
- v.push("a known divergence has an empty `ref` — 查不到出处的一条登记等于没登记");
347
+ // 没有票号形 `ref` / `window` / `flipWhen` 的一条登记没法被人查、也没法到期(棘轮靠这三个字段才立得住)。
348
+ // 类型上已经挡住(`kind` 只有 `"debt"`、`window` 必填),但适配器可能来自 JSON / `as` —— 门不靠类型自证,运行期照判;
349
+ // 字段先读进宽类型的局部,否则 TS 把「不是 debt」那一支收成 `never`,连报错文案都取不出字段。
350
+ const ledger = adapter.knownDivergences ?? [];
351
+ const nowMs = opts.nowMs;
352
+ const today = typeof nowMs === "number" && Number.isFinite(nowMs) ? new Date(nowMs).toISOString().slice(0, 10) : undefined;
353
+ if (ledger.length > 0 && today === undefined)
354
+ v.push("release mode needs `nowMs` to judge the ledger windows — 库码不读环境时钟,钟由调用方传(账本为空时不需要)");
355
+ for (const e of ledger) {
356
+ const ref = e.ref;
357
+ const kind = e.kind;
358
+ const window = e.window;
359
+ const name = typeof ref === "string" ? ref : String(ref);
360
+ if (typeof ref !== "string" || !LEDGER_REF_RULE.test(ref))
361
+ v.push(`known divergence \`ref\` ${JSON.stringify(ref)} is not a ticket id (\`ABC-12\` / \`ABC-12②\` / \`XY-7.3\` form) — 查不到出处的一条登记等于没登记`);
362
+ if (kind !== "debt")
363
+ v.push(`known divergence \`${name}\` has kind \`${String(kind)}\` — the ledger takes only debts (a ticket and a window), no standing exemptions`);
364
+ if (window === undefined)
365
+ v.push(`known divergence \`${name}\` has no \`window\` — 说不出最晚活到哪天的债永远不会被还`);
366
+ else if (!isCalendarDate(window))
367
+ v.push(`known divergence \`${name}\`: \`window\` ${JSON.stringify(window)} is not a calendar date (YYYY-MM-DD, UTC)`);
368
+ else if (today !== undefined && today > window)
369
+ v.push(`known divergence \`${name}\` is past its window ${window} (today ${today}) — pay the debt, or move the window in the ticket with a reason`);
319
370
  if (e.flipWhen.trim().length === 0)
320
- v.push(`known divergence \`${e.ref}\` has an empty \`flipWhen\` — 说不出什么时候删,它就永远不会被删`);
371
+ v.push(`known divergence \`${name}\` has an empty \`flipWhen\` — 说不出什么时候删,它就永远不会被删`);
321
372
  if (e.outcome === undefined) {
322
373
  if (!e.pathPrefix.startsWith("/") || e.pathPrefix.length < 2)
323
- v.push(`known divergence \`${e.ref}\` has a blanket \`pathPrefix\` (\`${e.pathPrefix}\`) — 账本要逐坐标点名,整片豁免不是账本`);
374
+ v.push(`known divergence \`${name}\` has a blanket \`pathPrefix\` (\`${e.pathPrefix}\`) — 账本要逐坐标点名,整片豁免不是账本`);
324
375
  continue;
325
376
  }
326
- // 结局形那一臂的两条硬约束。类型上已经挡住(`kind` 只有 `"debt"`、`cases` 必填),但适配器可能来自
327
- // JSON / `as` —— 门不靠类型自证,运行期照判。`kind` 先读进一个宽类型的局部,否则 TS 把「不是 debt」
328
- // 那一支收成 `never`,连报错文案都取不出字段。
329
- const kind = e.kind;
330
377
  const cases = e.outcome.cases;
331
- if (kind !== "debt")
332
- v.push(`known divergence \`${e.ref}\` is an outcome divergence with kind \`${kind}\` — 结局形只能是 \`debt\`:契约说拒、消费方说不拒,不是「成文语义」盖得住的分歧`);
333
378
  if (!Array.isArray(cases) || cases.length === 0)
334
- v.push(`known divergence \`${e.ref}\` is an outcome divergence that must name at least one case — 结局形不许按前缀大包一整条腿的拒绝面`);
379
+ v.push(`known divergence \`${name}\` is an outcome divergence that must name at least one case — 结局形不许按前缀大包一整条腿的拒绝面`);
335
380
  }
336
381
  // **每条腿都要表态**:实现,或显式声明不承担(闭集理由词 + why)。缺表态 / 形不对 ⇒ 违例 —— 不许静默跳过,也不许默认当通过。
337
382
  const legs = [];
@@ -453,8 +498,8 @@ export async function runConformance(adapter, opts) {
453
498
  for (const r of results)
454
499
  summary[r.status] += 1;
455
500
  const ledgerRows = ledger.map((e, i) => e.outcome === undefined
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] }
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] });
501
+ ? { ref: e.ref, kind: e.kind, window: e.window, leg: e.leg, pathPrefix: e.pathPrefix, ...(e.cases === undefined ? {} : { cases: e.cases }), flipWhen: e.flipWhen, hits: hits[i] }
502
+ : { ref: e.ref, kind: e.kind, window: e.window, leg: e.outcome.leg, outcome: { expected: e.outcome.expected, actual: e.outcome.actual }, cases: e.outcome.cases, flipWhen: e.flipWhen, hits: hits[i] });
458
503
  if (opts.mode === "release") {
459
504
  // **实现的腿必须跑**:实现了却在整套语料上一格真读数都没有,那份实现是空的。
460
505
  for (const leg of LEG_NAMES) {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 成案:2.0.0 / 2.1.0 两版的正确性靠「表态 + 黑盒测试 + 提货车各自红先」,三样都是**单仓视角** ——
5
5
  * 真跨仓分歧是被提货车**碰巧**撞出来的。碰巧不是门。本 kit 让每个消费方把**自己的生产读腿**塞进来跑
6
- * 同一份语料:同一份 settings 在 center / worker / 管理面 / 契约仓自身读出的 effective 逐字节一致、
6
+ * 同一份语料:同一份 settings 在 web-admin / worker / 管理面 / 契约仓自身读出的 effective 逐字节一致、
7
7
  * 告警多重集一致,否则红。
8
8
  *
9
9
  * 规范化(canonical JSON、告警投影、坐标深相等)全部在 kit 内**单铸** —— 消费方零复述,因为一份被各自
@@ -13,11 +13,12 @@ import type { EffectiveReadWarning, WarningProjection } from "../config-fns.js";
13
13
  /** 四条腿的名字。**闭集** —— 新开一条腿要同时在 `derive.ts` 的 {@link LEG_EXPECTATION}(`Record<LegName, …>`,
14
14
  * 不表态即编译红)里说清它的期望从哪来,且每个消费方的 {@link ConformanceAdapter.legs} 都要对它表态(实现或声明不承担)。
15
15
  *
16
- * · `domains`:各域**原始文档**(center 侧存储形)→ effective。契约仓的 `buildEffective`(读:逐条抢救 + 降级 + 告警)。
17
- * · `strictWrite`:同一份各域原始文档 → **严写受理**(写入席收下这份文档集与否)。契约仓的 `parseDomain`;管理面的
18
- * 域写受理。结局复用 {@link LegOutcome} 的两类:全部受理 ⇒ `effective` = 严格解析出的域文档集(只含入参里的键,
19
- * 不补缺席域);任何一份不受理 ⇒ `rejected{stage:"validate"}`。读腿抢救的(坏条目丢弃、整域降级、主机名夹紧)
20
- * 在这条腿上是拒 —— 写入席不替运维删他的条目。
16
+ * · `domains`:各域**原始文档**(web-admin 侧存储形)→ effective。契约仓的 `buildEffective`(读:逐条抢救 + 降级 + 告警)。
17
+ * · `strictWrite`:同一份各域原始文档 → **严写受理**(写入席收下这份文档集与否)。契约仓 = 逐域 `parseDomainLoud` + 整份文档集过一次
18
+ * 引用判官 `danglingRefs`;管理面 = 它的域写受理。结局复用 {@link LegOutcome} 的两类:全部受理 ⇒ `effective`
19
+ * = 严格解析出的域文档集(只含入参里的键,不补缺席域)+ 受理时的告警(剥掉 / 承运的未知键);任何一份不受理 ⇒
20
+ * `rejected{stage:"validate"}`;都解析得了但有悬空引用 ⇒ `rejected{stage:"validate", code:`{@link REF_INTEGRITY_REJECT_CODE}`}`,
21
+ * 拒体带悬空引用的坐标(`refIssues`)。读腿抢救的(坏条目丢弃、整域降级、主机名夹紧)在这条腿上是拒 —— 写入席不替运维删他的条目。
21
22
  * · `wire`:`GET /effective` 载荷 → effective。契约仓的 `readEffectiveWire`。
22
23
  * · `localRoot`:`config.d/` 目录快照(文件名 → 文件内容)→ effective。契约仓的 `loadLocalConfig`。 */
23
24
  export type LegName = "domains" | "strictWrite" | "wire" | "localRoot";
@@ -63,7 +64,15 @@ export type LegOutcome = {
63
64
  stage: RejectStage;
64
65
  code?: string;
65
66
  diagnostics: string;
67
+ refIssues?: readonly RefIssueCoordinate[];
66
68
  };
69
+ /** 严写腿因**悬空引用**拒收时的拒绝码(机读;金样与比对都按它)。 */
70
+ export declare const REF_INTEGRITY_REJECT_CODE = "ref-integrity";
71
+ /** 一条悬空引用的坐标(段数组,以域键起手 —— `refIntegrityIssues` 的 `path`)。严写腿以 {@link REF_INTEGRITY_REJECT_CODE} 拒收时,
72
+ * 拒体带它们的**多重集**;金样同存,比对按坐标(句子不比:句子可随实现改字)。 */
73
+ export interface RefIssueCoordinate {
74
+ path: readonly (string | number)[];
75
+ }
67
76
  /** 拒绝发生的层。闭集 —— 新层要来这里登记(语料的 `stage` 字段按本表校验)。 */
68
77
  export type RejectStage = "envelope" | "parse" | "validate" | "hash" | "store";
69
78
  /** {@link RejectStage} 的运行期表(语料校验用;与类型同源)。 */
@@ -136,7 +145,8 @@ export interface LegDeclined {
136
145
  * release 门在运行期按同一条律判(适配器可能来自 JS / `as`):带 `declined` 又带 `entry` 或 `run` = 形不对。 */
137
146
  export type LegStance = ConformanceLeg | LegDeclined;
138
147
  /**
139
- * 消费方对金样的一条**已知合法差异**的登记位 —— 临时欠账,或成文语义。
148
+ * 消费方对金样的一条**已知差异**的登记位 —— 一笔**债**(11.0.0 起只有这一种:删了「成文语义」`declared`,它没有到期,一行登记活过了
149
+ * 它指向的票)。每行带**票号形**的 `ref`(查得到出处)与 `window`(最晚活到哪天),release 模式过窗即违例。
140
150
  *
141
151
  * 成案:消费方对金样的合法差异今天只能写在测试注里,而散文不是判据 —— 谁也说不清「这条差异还在不在」。
142
152
  * 登记之后它变成机器可核的一行:命中即 `diff-ledgered`(不阻塞),**从未命中即违例**(`stale ledger entry`)。
@@ -158,11 +168,13 @@ export interface PathDivergence {
158
168
  * 登记 `/models/1` 不豁免 `/models/10`)。必须以 `/` 开头并**点名至少一个顶层键** —— `/` 或空串是
159
169
  * 一个 `ok=true` 开关,不是账本。 */
160
170
  pathPrefix: string;
161
- /** `"debt"` = 临时欠账(`ref` 是台账号);`"declared"` = 消费方成文语义(`ref` 是契约 / 文档锚)。 */
162
- kind: "debt" | "declared";
163
- /** 台账号或文档锚 —— 报告按它分组,人照它去查这笔差异的出处。 */
171
+ /** 账本只有债。 */
172
+ kind: "debt";
173
+ /** 台账票号({@link LEDGER_REF_RULE}:`ABC-12` / `ABC-12②` / `XY-7.3` 这一形,不是一段散文)—— 报告按它分组,人照它去查出处。 */
164
174
  ref: string;
165
- /** 什么时候该把这一条删掉(债还了 / 语义改了)。人读,非空。 */
175
+ /** 这笔债最晚活到哪天:`YYYY-MM-DD`(UTC 日历日,当天仍在窗内)。release 模式的钟({@link ConformanceOptions.nowMs})晚于它 ⇒ 违例。 */
176
+ window: string;
177
+ /** 什么时候该把这一条删掉(债还了)。人读,非空。 */
166
178
  flipWhen: string;
167
179
  outcome?: never;
168
180
  }
@@ -172,10 +184,7 @@ export interface PathDivergence {
172
184
  * 成案(接线实跑):这类差根本没有 effective patch —— 比对停在「结局形就不一样」那一步,坐标形账本
173
185
  * 按 `pathPrefix` 够不着它,于是消费方 `ok` 恒 false,而它恰恰是门最该点名、也最该逼着修的一类。
174
186
  *
175
- * 两条硬约束,都是为了不让它变成一张万能豁免:
176
- * · **`cases` 必填且非空** —— 结局形分歧不许按前缀大包一整条腿的拒绝面;
177
- * · **只能是 `kind:"debt"`** —— 契约说拒、消费方说不拒,这不是「成文语义」盖得住的分歧,只能是债,
178
- * 带台账号、带翻转条件,由 stale 律逼着到期删掉。
187
+ * 硬约束:**`cases` 必填且非空** —— 结局形分歧不许按前缀大包一整条腿的拒绝面。与坐标形同律:是债,带票号、带窗、带翻转条件。
179
188
  */
180
189
  export interface OutcomeDivergence {
181
190
  kind: "debt";
@@ -189,10 +198,14 @@ export interface OutcomeDivergence {
189
198
  actual: "rejected" | "effective";
190
199
  };
191
200
  ref: string;
201
+ window: string;
192
202
  flipWhen: string;
193
203
  leg?: never;
194
204
  pathPrefix?: never;
195
205
  }
206
+ /** 账本 `ref` 的形 —— 一个台账票号:大写字母起头、`-` 分段、带数字,可带一个小数段与至多两个字符的尾缀(`ABC-12`、`ABC-12②`、
207
+ * `AB-C3`、`XY-7.3`),没有空白。一段散文(「一个设计稿的章节号 — …」)不是票号:查不到、也到不了期。 */
208
+ export declare const LEDGER_REF_RULE: RegExp;
196
209
  /** 一个消费方交给 kit 的东西:它是谁、它加载的契约包是哪一份、它有哪几条生产读腿。 */
197
210
  export interface ConformanceAdapter {
198
211
  /** 消费方名(语料 `contributor` 同词表:`server` / `web-admin` / `cli` / `client-core` / `test` / `contract`)。 */
@@ -282,6 +295,7 @@ export interface ConformanceResult {
282
295
  export interface LedgerRow {
283
296
  ref: string;
284
297
  kind: KnownDivergence["kind"];
298
+ window: string;
285
299
  leg: LegName;
286
300
  /** 坐标形登记的段前缀(见 {@link PathDivergence.pathPrefix});**结局形**登记没有这一项。 */
287
301
  pathPrefix?: string;
@@ -334,4 +348,6 @@ export interface ConformanceOptions {
334
348
  corpusDir?: string;
335
349
  /** historical 模式的归一器。release 模式传了即违例(归一器只用来看旧版,不用来放宽今天的门)。 */
336
350
  normalizer?: ConformanceNormalizer;
351
+ /** 判账本窗的钟(epoch ms)。库码不读环境时钟:release 模式下适配器挂着账本 ⇒ 必须传,否则违例;账本为空不需要。 */
352
+ nowMs?: number;
337
353
  }
@@ -1,6 +1,11 @@
1
1
  /** 四条腿的名字表(运行期枚举用;与 {@link LegName} 同源 —— 多一员少一员编译红)。 */
2
2
  export const LEG_NAMES = ["domains", "strictWrite", "wire", "localRoot"];
3
+ /** 严写腿因**悬空引用**拒收时的拒绝码(机读;金样与比对都按它)。 */
4
+ export const REF_INTEGRITY_REJECT_CODE = "ref-integrity";
3
5
  /** {@link RejectStage} 的运行期表(语料校验用;与类型同源)。 */
4
6
  export const REJECT_STAGES = ["envelope", "parse", "validate", "hash", "store"];
5
7
  /** {@link LegDeclineReason} 的运行期表(release 门校验用;与类型同源)。 */
6
8
  export const LEG_DECLINE_REASONS = ["no-production-path"];
9
+ /** 账本 `ref` 的形 —— 一个台账票号:大写字母起头、`-` 分段、带数字,可带一个小数段与至多两个字符的尾缀(`ABC-12`、`ABC-12②`、
10
+ * `AB-C3`、`XY-7.3`),没有空白。一段散文(「一个设计稿的章节号 — …」)不是票号:查不到、也到不了期。 */
11
+ export const LEDGER_REF_RULE = /^(?=[^\s]*\d)[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+(?:\.[0-9]+)?[^\s]{0,2}$/;
@@ -17,7 +17,7 @@ import type { ConformanceNormalizer } from "./types.js";
17
17
  export { WARNING_KINDS, projectWarning, type WarningKind, type WarningProjection } from "../config-fns.js";
18
18
  /**
19
19
  * 一条投影的**比对归一形**:①坐标过一遍映射(historical 模式把段拼成旧版的点号串;无归一器时是恒等);
20
- * ②`keys` / `droppedNames` 这两个**集合字段**按袋归一。第二处闭集穷举:哪些 kind 带坐标、带在哪个字段上,
20
+ * ②`keys` 这个**集合字段**按袋归一。第二处闭集穷举:哪些 kind 带坐标、带在哪个字段上,
21
21
  * 写在这里一次。
22
22
  *
23
23
  * 为什么集合字段要按袋比(codex R1 [medium] 复现):一条告警里的 `keys` 是「这些键被剥了」的**一只袋**,
@@ -22,7 +22,7 @@ export { WARNING_KINDS, projectWarning } from "../config-fns.js";
22
22
  const bag = (items) => [...items].sort((a, b) => (stableKey(a) < stableKey(b) ? -1 : stableKey(a) > stableKey(b) ? 1 : 0));
23
23
  /**
24
24
  * 一条投影的**比对归一形**:①坐标过一遍映射(historical 模式把段拼成旧版的点号串;无归一器时是恒等);
25
- * ②`keys` / `droppedNames` 这两个**集合字段**按袋归一。第二处闭集穷举:哪些 kind 带坐标、带在哪个字段上,
25
+ * ②`keys` 这个**集合字段**按袋归一。第二处闭集穷举:哪些 kind 带坐标、带在哪个字段上,
26
26
  * 写在这里一次。
27
27
  *
28
28
  * 为什么集合字段要按袋比(codex R1 [medium] 复现):一条告警里的 `keys` 是「这些键被剥了」的**一只袋**,
@@ -34,12 +34,9 @@ export function normalizeProjection(p, f) {
34
34
  switch (p.kind) {
35
35
  case "unknown-keys-dropped":
36
36
  case "unknown-keys-carried":
37
- case "legacy-governance-lifted":
38
37
  return { ...p, keys: bag(p.keys.map(f)) };
39
38
  case "entry-dropped":
40
39
  return { ...p, path: f(p.path) };
41
- case "hosts-grandfathered":
42
- return { ...p, droppedNames: bag(p.droppedNames) };
43
40
  case "domain-defaulted":
44
41
  case "unread-config-file":
45
42
  return p;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Cross-domain referential integrity — the PURE half, shared by the single-domain PUT and the batch write so
3
3
  * BOTH enforce the same rules (the multi-roster design note §6). The FLEET-coupled `applyAutoPlacement` (which imports
4
- * `../fleet/placement`) STAYS in sema-registry — it's not pure and not portable. This package carries only
4
+ * `../fleet/placement`) STAYS in web-admin — it's not pure and not portable. This package carries only
5
5
  * `refIntegrityIssues` + `createSiblingResolver` + the ref-issue types.
6
6
  *
7
7
  * BATCH-AWARE: a sibling domain (the models a roster references, the rosters a worker references) may be
@@ -12,11 +12,43 @@
12
12
  */
13
13
  import type { ConfigReader } from "./reader.js";
14
14
  import type { DomainConfig } from "./types.js";
15
+ /** What a dangling reference failed to find —— the closed set (S-1075, 11.0.0): one word per referenced domain. */
16
+ export declare const REF_ISSUE_KINDS: readonly ["unknown_model", "unknown_roster", "unknown_worker", "unknown_skill", "unknown_mcp", "unknown_plugin"];
17
+ export type RefIssueKind = (typeof REF_ISSUE_KINDS)[number];
18
+ /** The domain a {@link RefIssueKind}'s target lives in. */
19
+ export declare const REF_ISSUE_TARGET_DOMAIN: {
20
+ readonly unknown_model: "models";
21
+ readonly unknown_roster: "rosters";
22
+ readonly unknown_worker: "workers";
23
+ readonly unknown_skill: "skills";
24
+ readonly unknown_mcp: "mcp";
25
+ readonly unknown_plugin: "plugins";
26
+ };
27
+ /** The domains whose documents carry cross-domain references (by name) —— the referrer side of the judge. */
28
+ export declare const REF_REFERRER_DOMAINS: readonly ["rosters", "workers", "models", "collab", "systems", "entitlement"];
29
+ export type RefReferrerDomain = (typeof REF_REFERRER_DOMAINS)[number];
30
+ /** Who holds the reference: the referring entry's own name / id, or — for a domain-level field (`models.roles`,
31
+ * `models.atMention`) — the domain alone (`name` absent). */
32
+ export interface RefReferrer {
33
+ domain: RefReferrerDomain;
34
+ name?: string;
35
+ }
36
+ /** One reference that does not resolve —— structured operands (S-1075, 11.0.0): `kind` (what was not found), `referrer` (who
37
+ * holds the reference), `target` (the name that was not found), `path` (where the reference sits; `path[0]` = the referrer's
38
+ * domain key). `message` is {@link renderRefIssue} of them — a reader that must treat the user-written values differently from
39
+ * the sentence's fixed words (a log boundary fingerprinting each value) re-renders with its own `show` instead of parsing the
40
+ * sentence. Before 11.0.0 the values were only inside `message`. */
15
41
  export interface RefIssue {
16
42
  path: (string | number)[];
43
+ code: "custom";
44
+ kind: RefIssueKind;
45
+ referrer: RefReferrer;
46
+ target: string;
17
47
  message: string;
18
- code: string;
19
48
  }
49
+ /** The ONE rendering of a {@link RefIssue}'s sentence: `<referrer> → unknown <noun> "<target>" (at <path>)`. `show` maps each
50
+ * user-written value (the referrer's name, the target); the default shows them as written — that is how `message` is minted. */
51
+ export declare function renderRefIssue(issue: Omit<RefIssue, "message">, show?: (value: string) => string): string;
20
52
  /** Resolve a sibling domain's effective value for integrity checks. */
21
53
  export type SiblingResolver = <K extends keyof DomainConfig>(domain: K) => Promise<DomainConfig[K] | undefined>;
22
54
  /** A resolver over the reader, optionally shadowed by pending (same-operation) writes keyed by domain.
@@ -29,30 +61,42 @@ export type SiblingResolver = <K extends keyof DomainConfig>(domain: K) => Promi
29
61
  export declare function createSiblingResolver(store: ConfigReader, pending?: Partial<{
30
62
  [K in keyof DomainConfig]: DomainConfig[K];
31
63
  }>): SiblingResolver;
32
- /** The domains whose writes carry cross-domain model/roster references we validate at save time — the closed
33
- * set as a RUNTIME VALUE (4.0.0). A consumer that has to ask "is this domain ref-checked?" at runtime (a
34
- * config-rollback path deciding whether to run the guard before writing) could only hand-copy the list
35
- * before, and a hand copy drifts silently: two rollback routes in the admin console each carried a
36
- * five-member copy that had lost `entitlement`, so an entitlement rollback skipped the guard entirely with
37
- * nothing red anywhere. Ask this constant (or {@link isRefCheckedDomain}) instead of writing a list.
64
+ /** The domains a write must be judged for —— the closed set as a RUNTIME VALUE (4.0.0): every domain that HOLDS cross-domain
65
+ * references ({@link REF_REFERRER_DOMAINS}) and every domain those references POINT INTO (the targets of
66
+ * {@link REF_ISSUE_TARGET_DOMAIN}) —— 11.0.0 (S-1066 ③) added the three target-only domains `skills` / `mcp` / `plugins`: deleting
67
+ * a skill a tier still names is a dangling reference made by the skills write, so that write is judged too. A consumer that has
68
+ * to ask "is this domain ref-checked?" at runtime (a config-rollback path deciding whether to run the guard before writing)
69
+ * asks this constant (or {@link isRefCheckedDomain}) instead of writing a list — a hand copy drifts silently (two rollback routes
70
+ * in the admin console each once carried a five-member copy that had lost `entitlement`).
38
71
  *
39
- * `satisfies readonly (keyof DomainConfig)[]` keeps every member a real domain key; {@link RefCheckedDomain}
40
- * is DERIVED from this value, so there is exactly one place the set is written down. */
41
- export declare const REF_CHECKED_DOMAINS: readonly ["rosters", "workers", "models", "collab", "systems", "entitlement"];
42
- /** The domains whose writes carry cross-domain model/roster references we validate at save time.
43
- * DERIVED from {@link REF_CHECKED_DOMAINS} (4.0.0) — a hand-written union next to the value would be a
44
- * second copy of the same set, free to disagree with it. */
72
+ * `satisfies` keeps every member a real domain key; a pin checks it equals referrers ∪ targets. {@link RefCheckedDomain} is
73
+ * DERIVED from this value. */
74
+ export declare const REF_CHECKED_DOMAINS: readonly ["rosters", "workers", "models", "collab", "systems", "entitlement", "skills", "mcp", "plugins"];
75
+ /** A domain a write must be judged for. DERIVED from {@link REF_CHECKED_DOMAINS} (4.0.0). */
45
76
  export type RefCheckedDomain = (typeof REF_CHECKED_DOMAINS)[number];
46
77
  /** Runtime narrowing to {@link RefCheckedDomain}, same idiom as `isDomainName`: use it instead of hard-coding
47
78
  * the set when filtering an arbitrary domain key (a batch payload's keys, a rollback target). */
48
79
  export declare function isRefCheckedDomain(d: unknown): d is RefCheckedDomain;
49
- /** Cross-domain ref integrity for one {@link RefCheckedDomain} value. Every issue `path` starts with the
50
- * DOMAIN KEY (e.g. `["rosters", i, "models", j]`, `["collab", "templates", i, …]`) so a caller can prefix a
51
- * batch index and get a coordinate that is unambiguous across arms. Empty = ok. Runtime resolution stays
52
- * tolerant (falls back); this is purely a save-time guard — a precise 400 at save time is friendlier than a
53
- * silently-wrong roster.
80
+ /** The documents the judge reads: any subset of the {@link RefCheckedDomain} domains. A domain that is absent defines no names
81
+ * and holds no references (an absent `models` = an empty catalog: every model reference dangles). */
82
+ export type RefDocs = Partial<{
83
+ [K in RefCheckedDomain]: DomainConfig[K];
84
+ }>;
85
+ /**
86
+ * **THE reference judge** (11.0.0, S-1066 ③; pure, synchronous): every by-name cross-domain reference in a document set that does
87
+ * not resolve, as a {@link RefIssue} (`path` = where the reference sits, `path[0]` = the referrer's domain). Both seats read this
88
+ * one conclusion: the write seat ({@link refIntegrityIssues}, refusing) and the read side (`resolveEffectiveForWorker`'s
89
+ * `refIssues`, reporting what it drops). Before 11.0.0 the write seat judged forward only and the read side judged on its own,
90
+ * silently.
91
+ */
92
+ export declare function danglingRefs(docs: RefDocs): RefIssue[];
93
+ /** Cross-domain ref integrity for a write of one {@link RefCheckedDomain} value (the write seat): the judge's conclusion on the
94
+ * document set AFTER the write ({@link danglingRefs} over `value` + every other ref-checked domain through `resolve`), narrowed
95
+ * to the references this write is answerable for —— the ones its own document holds (forward: a roster naming a model that is not
96
+ * in the catalog) and the ones pointing into it (reverse, 11.0.0: deleting a model a roster still names; the issue names the
97
+ * roster, `path` points at the reference). Empty = ok.
54
98
  *
55
- * 4.0.0 made that first sentence TRUE: the `collab` arm used to start its paths at `"templates"` and the
56
- * `entitlement` arm at `"tiers"`, so a caller could not read `path[0]` as the domain. See the CHANGELOG entry
57
- * for the migration (BREAKING, value shape). */
99
+ * BATCH-AWARE through `resolve` (a sibling being written in the same batch shadows the stored one, see
100
+ * {@link createSiblingResolver}). A reference that already dangled before this write and still involves this domain is reported
101
+ * too — the same as the forward half always did (a roster write re-judges every roster). */
58
102
  export declare function refIntegrityIssues(domain: RefCheckedDomain, value: unknown, resolve: SiblingResolver): Promise<RefIssue[]>;