@sema-agent/settings-schema 2.1.0 → 3.0.1

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 (203) hide show
  1. package/CHANGELOG.md +253 -1
  2. package/README.md +45 -10
  3. package/conformance/VERSION +1 -0
  4. package/conformance/baseline.json +55 -0
  5. package/conformance/cases/c01-entry-salvage-then-typo/expected.json +105 -0
  6. package/conformance/cases/c01-entry-salvage-then-typo/input.json +22 -0
  7. package/conformance/cases/c01-entry-salvage-then-typo/meta.json +10 -0
  8. package/conformance/cases/c02-hosts-clamp-stacked/expected.json +96 -0
  9. package/conformance/cases/c02-hosts-clamp-stacked/input.json +21 -0
  10. package/conformance/cases/c02-hosts-clamp-stacked/meta.json +10 -0
  11. package/conformance/cases/c03-literal-dotted-top-key/expected.json +112 -0
  12. package/conformance/cases/c03-literal-dotted-top-key/input.json +26 -0
  13. package/conformance/cases/c03-literal-dotted-top-key/meta.json +5 -0
  14. package/conformance/cases/c04-all-bad-collection-not-salvaged/expected.json +77 -0
  15. package/conformance/cases/c04-all-bad-collection-not-salvaged/input.json +18 -0
  16. package/conformance/cases/c04-all-bad-collection-not-salvaged/meta.json +5 -0
  17. package/conformance/cases/c05-gate-domain-bad-entry-throws/expected.json +16 -0
  18. package/conformance/cases/c05-gate-domain-bad-entry-throws/input.json +18 -0
  19. package/conformance/cases/c05-gate-domain-bad-entry-throws/meta.json +5 -0
  20. package/conformance/cases/c06-legacy-governance-lift/expected.json +99 -0
  21. package/conformance/cases/c06-legacy-governance-lift/input.json +20 -0
  22. package/conformance/cases/c06-legacy-governance-lift/meta.json +9 -0
  23. package/conformance/cases/c07-hooks-is-not-a-config-domain/expected.json +72 -0
  24. package/conformance/cases/c07-hooks-is-not-a-config-domain/input.json +43 -0
  25. package/conformance/cases/c07-hooks-is-not-a-config-domain/meta.json +6 -0
  26. package/conformance/cases/c08-limits-unknown-carried-64/expected.json +336 -0
  27. package/conformance/cases/c08-limits-unknown-carried-64/input.json +74 -0
  28. package/conformance/cases/c08-limits-unknown-carried-64/meta.json +5 -0
  29. package/conformance/cases/c09-limits-unknown-carried-65/expected.json +338 -0
  30. package/conformance/cases/c09-limits-unknown-carried-65/input.json +75 -0
  31. package/conformance/cases/c09-limits-unknown-carried-65/meta.json +7 -0
  32. package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +106 -0
  33. package/conformance/cases/c10-record-key-secret-redacted-twice/input.json +29 -0
  34. package/conformance/cases/c10-record-key-secret-redacted-twice/meta.json +13 -0
  35. package/conformance/cases/c11-unicode-unknown-keys/expected.json +88 -0
  36. package/conformance/cases/c11-unicode-unknown-keys/input.json +13 -0
  37. package/conformance/cases/c11-unicode-unknown-keys/meta.json +5 -0
  38. package/conformance/cases/c12-prototype-name-top-keys/expected.json +72 -0
  39. package/conformance/cases/c12-prototype-name-top-keys/input.json +20 -0
  40. package/conformance/cases/c12-prototype-name-top-keys/meta.json +11 -0
  41. package/conformance/cases/c13-models-field-null/expected.json +77 -0
  42. package/conformance/cases/c13-models-field-null/input.json +11 -0
  43. package/conformance/cases/c13-models-field-null/meta.json +5 -0
  44. package/conformance/cases/c14-models-field-empty-array/expected.json +72 -0
  45. package/conformance/cases/c14-models-field-empty-array/input.json +11 -0
  46. package/conformance/cases/c14-models-field-empty-array/meta.json +5 -0
  47. package/conformance/cases/c15-models-field-number/expected.json +77 -0
  48. package/conformance/cases/c15-models-field-number/input.json +11 -0
  49. package/conformance/cases/c15-models-field-number/meta.json +5 -0
  50. package/conformance/cases/c16-deep-nesting-205/expected.json +83 -0
  51. package/conformance/cases/c16-deep-nesting-205/input.json +421 -0
  52. package/conformance/cases/c16-deep-nesting-205/meta.json +5 -0
  53. package/conformance/cases/c17-empty-domain-file/expected.json +73 -0
  54. package/conformance/cases/c17-empty-domain-file/input.json +10 -0
  55. package/conformance/cases/c17-empty-domain-file/meta.json +5 -0
  56. package/conformance/cases/c18-wire-envelope-not-an-object/expected.json +74 -0
  57. package/conformance/cases/c18-wire-envelope-not-an-object/input.json +8 -0
  58. package/conformance/cases/c18-wire-envelope-not-an-object/meta.json +5 -0
  59. package/conformance/cases/c19-wire-envelope-no-version/expected.json +74 -0
  60. package/conformance/cases/c19-wire-envelope-no-version/input.json +13 -0
  61. package/conformance/cases/c19-wire-envelope-no-version/meta.json +5 -0
  62. package/conformance/cases/c20-governance-absent-lifts/expected.json +84 -0
  63. package/conformance/cases/c20-governance-absent-lifts/input.json +11 -0
  64. package/conformance/cases/c20-governance-absent-lifts/meta.json +9 -0
  65. package/conformance/cases/c21-governance-empty-object-wins/expected.json +72 -0
  66. package/conformance/cases/c21-governance-empty-object-wins/input.json +12 -0
  67. package/conformance/cases/c21-governance-empty-object-wins/meta.json +5 -0
  68. package/conformance/cases/c22-governance-null-throws/expected.json +16 -0
  69. package/conformance/cases/c22-governance-null-throws/input.json +9 -0
  70. package/conformance/cases/c22-governance-null-throws/meta.json +5 -0
  71. package/conformance/cases/c23-mixed-validity-models/expected.json +106 -0
  72. package/conformance/cases/c23-mixed-validity-models/input.json +27 -0
  73. package/conformance/cases/c23-mixed-validity-models/meta.json +9 -0
  74. package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +100 -0
  75. package/conformance/cases/c24-valid-entries-with-unknown-keys/input.json +25 -0
  76. package/conformance/cases/c24-valid-entries-with-unknown-keys/meta.json +5 -0
  77. package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +89 -0
  78. package/conformance/cases/c25-published-roundtrip-zero-warnings/input.json +24 -0
  79. package/conformance/cases/c25-published-roundtrip-zero-warnings/meta.json +5 -0
  80. package/conformance/cases/c26-hosts-array-1024/expected.json +7241 -0
  81. package/conformance/cases/c26-hosts-array-1024/input.json +3084 -0
  82. package/conformance/cases/c26-hosts-array-1024/meta.json +5 -0
  83. package/conformance/cases/c27-hosts-array-1025/expected.json +7248 -0
  84. package/conformance/cases/c27-hosts-array-1025/input.json +3087 -0
  85. package/conformance/cases/c27-hosts-array-1025/meta.json +5 -0
  86. package/conformance/cases/c28-large-document-over-1mb/expected.json +82 -0
  87. package/conformance/cases/c28-large-document-over-1mb/input.json +11 -0
  88. package/conformance/cases/c28-large-document-over-1mb/meta.json +5 -0
  89. package/conformance/cases/c29-salvage-then-domain-refine-fails/expected.json +77 -0
  90. package/conformance/cases/c29-salvage-then-domain-refine-fails/input.json +27 -0
  91. package/conformance/cases/c29-salvage-then-domain-refine-fails/meta.json +5 -0
  92. package/conformance/cases/c30-version-triple-a-first/expected.json +85 -0
  93. package/conformance/cases/c30-version-triple-a-first/input.json +18 -0
  94. package/conformance/cases/c30-version-triple-a-first/meta.json +5 -0
  95. package/conformance/cases/c31-version-triple-b-changed/expected.json +85 -0
  96. package/conformance/cases/c31-version-triple-b-changed/input.json +18 -0
  97. package/conformance/cases/c31-version-triple-b-changed/meta.json +5 -0
  98. package/conformance/cases/c32-version-triple-a-restored/expected.json +85 -0
  99. package/conformance/cases/c32-version-triple-a-restored/input.json +18 -0
  100. package/conformance/cases/c32-version-triple-a-restored/meta.json +5 -0
  101. package/conformance/cases/c33-limits-nested-carried-key/expected.json +91 -0
  102. package/conformance/cases/c33-limits-nested-carried-key/input.json +14 -0
  103. package/conformance/cases/c33-limits-nested-carried-key/meta.json +5 -0
  104. package/conformance/cases/c34-skills-content-vs-manifest/expected.json +160 -0
  105. package/conformance/cases/c34-skills-content-vs-manifest/input.json +18 -0
  106. package/conformance/cases/c34-skills-content-vs-manifest/meta.json +5 -0
  107. package/conformance/cases/c35-portable-domain-file-is-null/expected.json +76 -0
  108. package/conformance/cases/c35-portable-domain-file-is-null/input.json +9 -0
  109. package/conformance/cases/c35-portable-domain-file-is-null/meta.json +5 -0
  110. package/conformance/cases/c36-skills-entry-without-content/expected.json +144 -0
  111. package/conformance/cases/c36-skills-entry-without-content/input.json +16 -0
  112. package/conformance/cases/c36-skills-entry-without-content/meta.json +5 -0
  113. package/conformance/cases/c37-skills-field-null/expected.json +134 -0
  114. package/conformance/cases/c37-skills-field-null/input.json +9 -0
  115. package/conformance/cases/c37-skills-field-null/meta.json +5 -0
  116. package/conformance/cases/c38-remote-exec-is-a-non-domain-file/expected.json +72 -0
  117. package/conformance/cases/c38-remote-exec-is-a-non-domain-file/input.json +11 -0
  118. package/conformance/cases/c38-remote-exec-is-a-non-domain-file/meta.json +5 -0
  119. package/conformance/cases/c39-hidden-file-in-config-d/expected.json +72 -0
  120. package/conformance/cases/c39-hidden-file-in-config-d/input.json +11 -0
  121. package/conformance/cases/c39-hidden-file-in-config-d/meta.json +5 -0
  122. package/conformance/cases/cc01-mcp-toolfaces-non-object/expected.json +112 -0
  123. package/conformance/cases/cc01-mcp-toolfaces-non-object/input.json +39 -0
  124. package/conformance/cases/cc01-mcp-toolfaces-non-object/meta.json +8 -0
  125. package/conformance/cases/cc02-mcp-source-key-is-transported/expected.json +85 -0
  126. package/conformance/cases/cc02-mcp-source-key-is-transported/input.json +20 -0
  127. package/conformance/cases/cc02-mcp-source-key-is-transported/meta.json +8 -0
  128. package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +89 -0
  129. package/conformance/cases/cc03-model-compat-unknown-subkey/input.json +22 -0
  130. package/conformance/cases/cc03-model-compat-unknown-subkey/meta.json +8 -0
  131. package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +102 -0
  132. package/conformance/cases/cli01-model-name-nfc-vs-nfd/input.json +30 -0
  133. package/conformance/cases/cli01-model-name-nfc-vs-nfd/meta.json +9 -0
  134. package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +115 -0
  135. package/conformance/cases/cli02-control-and-zero-width-chars/input.json +42 -0
  136. package/conformance/cases/cli02-control-and-zero-width-chars/meta.json +9 -0
  137. package/conformance/cases/cli03-credential-pasted-into-unknown-key/expected.json +92 -0
  138. package/conformance/cases/cli03-credential-pasted-into-unknown-key/input.json +16 -0
  139. package/conformance/cases/cli03-credential-pasted-into-unknown-key/meta.json +9 -0
  140. package/conformance/cases/cli04-env-value-with-newline/expected.json +93 -0
  141. package/conformance/cases/cli04-env-value-with-newline/input.json +23 -0
  142. package/conformance/cases/cli04-env-value-with-newline/meta.json +5 -0
  143. package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +109 -0
  144. package/conformance/cases/cli05-long-and-prefix-colliding-names/input.json +30 -0
  145. package/conformance/cases/cli05-long-and-prefix-colliding-names/meta.json +5 -0
  146. package/conformance/cases/t01-absent-domain-key-not-null/expected.json +93 -0
  147. package/conformance/cases/t01-absent-domain-key-not-null/input.json +29 -0
  148. package/conformance/cases/t01-absent-domain-key-not-null/meta.json +6 -0
  149. package/conformance/cases/t02-entry-salvage-vs-whole-domain-refusal/expected.json +16 -0
  150. package/conformance/cases/t02-entry-salvage-vs-whole-domain-refusal/input.json +32 -0
  151. package/conformance/cases/t02-entry-salvage-vs-whole-domain-refusal/meta.json +6 -0
  152. package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +107 -0
  153. package/conformance/cases/t03-prototype-name-key-not-swallowed/input.json +25 -0
  154. package/conformance/cases/t03-prototype-name-key-not-swallowed/meta.json +6 -0
  155. package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +105 -0
  156. package/conformance/cases/t04-secret-literal-never-in-warning-value/input.json +25 -0
  157. package/conformance/cases/t04-secret-literal-never-in-warning-value/meta.json +14 -0
  158. package/conformance/cases/z01-every-domain-empty-object/expected.json +72 -0
  159. package/conformance/cases/z01-every-domain-empty-object/input.json +25 -0
  160. package/conformance/cases/z01-every-domain-empty-object/meta.json +5 -0
  161. package/conformance/cases/z02-every-domain-unknown-key/expected.json +228 -0
  162. package/conformance/cases/z02-every-domain-unknown-key/input.json +59 -0
  163. package/conformance/cases/z02-every-domain-unknown-key/meta.json +5 -0
  164. package/conformance/historical/settings-schema@1.0.0.json +1080 -0
  165. package/conformance/historical/settings-schema@1.10.0.json +1080 -0
  166. package/conformance/historical/settings-schema@1.5.0.json +1080 -0
  167. package/conformance/historical/settings-schema@2.0.0.json +1080 -0
  168. package/conformance/historical/settings-schema@2.1.0.json +1080 -0
  169. package/dist/api/auth.d.ts +42 -1
  170. package/dist/api/auth.js +71 -4
  171. package/dist/api/scopes.d.ts +9 -1
  172. package/dist/api/scopes.js +8 -2
  173. package/dist/config-fns.d.ts +60 -28
  174. package/dist/config-fns.js +94 -77
  175. package/dist/conformance/canonical.d.ts +36 -0
  176. package/dist/conformance/canonical.js +114 -0
  177. package/dist/conformance/contract-adapter.d.ts +20 -0
  178. package/dist/conformance/contract-adapter.js +74 -0
  179. package/dist/conformance/corpus.d.ts +73 -0
  180. package/dist/conformance/corpus.js +99 -0
  181. package/dist/conformance/derive.d.ts +48 -0
  182. package/dist/conformance/derive.js +136 -0
  183. package/dist/conformance/index.d.ts +22 -0
  184. package/dist/conformance/index.js +22 -0
  185. package/dist/conformance/run.d.ts +27 -0
  186. package/dist/conformance/run.js +424 -0
  187. package/dist/conformance/types.d.ts +288 -0
  188. package/dist/conformance/types.js +4 -0
  189. package/dist/conformance/warnings.d.ts +88 -0
  190. package/dist/conformance/warnings.js +144 -0
  191. package/dist/cross-domain.d.ts +1 -1
  192. package/dist/cross-domain.js +1 -1
  193. package/dist/file-store.d.ts +7 -0
  194. package/dist/file-store.js +6 -2
  195. package/dist/hooks.d.ts +2 -2
  196. package/dist/hooks.js +2 -2
  197. package/dist/scheduler-store.d.ts +1 -1
  198. package/dist/scheduler-store.js +1 -1
  199. package/dist/types.d.ts +2 -2
  200. package/dist/types.js +3 -3
  201. package/package.json +7 -5
  202. package/dist/api/wire.d.ts +0 -60
  203. package/dist/api/wire.js +0 -89
@@ -0,0 +1,424 @@
1
+ /**
2
+ * 驱动:一套语料 × 一个适配器的 N 条读腿 → 一份报告。
3
+ *
4
+ * 四律在这里落地(缺一不算「门」):
5
+ * · **对抗律**——语料自带 `claim` / `firstRedOn`,由 `test/conformance-corpus.test.ts` 计数(本文件只保证
6
+ * 它们被读进来、坏形立刻抛)。
7
+ * · **变异律**——本驱动是被变异的那一面:契约任一铸点被改坏,语料必红(`test/conformance-mutation.test.ts`)。
8
+ * · **覆盖律**——报告逐 case × 腿出结局,覆盖矩阵按它计数(`test/conformance-coverage.test.ts`)。
9
+ * · **来源律**——release 模式核 provenance 与每条腿的生产入口({@link releaseViolations})。
10
+ *
11
+ * 比对分三面,刻意不混:**effective**(剥掉元数据后结构化深相等)、**告警多重集**、**元数据**
12
+ * (`version`/`updatedAt` 由 harness 注入或由腿自己算,独立断言 —— 不让 canonical 把它盖过去)。
13
+ */
14
+ import { mkdtemp, mkdir, readFile, realpath, rm, stat, writeFile, utimes } from "node:fs/promises";
15
+ import { tmpdir } from "node:os";
16
+ import { basename, extname, isAbsolute, join, resolve, sep } from "node:path";
17
+ import { canonicalize, deepEqual, diffJson, stableKey } from "./canonical.js";
18
+ import { diffWarningMultiset, findSecretLeaks, projectWarning } from "./warnings.js";
19
+ import { deriveLocalExpectation, deriveLocalInput, deriveWireExpectation, deriveWireInput, toManifestDoc } from "./derive.js";
20
+ import { kitIdentity, loadCorpus } from "./corpus.js";
21
+ import { LEG_NAMES, } from "./types.js";
22
+ /** 一个**模块**文件的样子 —— 来源律用它挡住「随便找个存在的文件当入口」(`package.json` 也是文件)。 */
23
+ const MODULE_EXTENSIONS = [".ts", ".mts", ".cts", ".tsx", ".js", ".mjs", ".cjs", ".jsx"];
24
+ /** 契约包装在 `node_modules` 里的样子 —— 来源律用它判「这条腿其实就是契约包自己」。按**包名**拼,
25
+ * 不手抄(包名从 kit 自己的 package.json 来)。 */
26
+ const contractInNodeModules = (packageName) => `${sep}node_modules${sep}${packageName.split("/").join(sep)}${sep}`;
27
+ /**
28
+ * 把一份 `config.d/` 快照落成真目录 —— **kit 单铸**,消费方零复述(每个消费方各写一遍落盘逻辑,
29
+ * mtime 怎么设、目录怎么建就会各不相同,而 `updatedAt` 正是按 mtime 算的)。
30
+ *
31
+ * 返回 `root` 与 `cleanup`;所有文件的 mtime 统一设成 `mtime`,于是 `updatedAt` 对同一份快照恒定。
32
+ */
33
+ export async function materializeLocalRoot(files, mtime) {
34
+ // 闸**先于**任何 fs 动作:一个坏名字不许留下半个临时根(「建了再清」比这多一条「清到一半又抛」的路)。
35
+ for (const name of Object.keys(files))
36
+ assertBareFileName(name);
37
+ const root = await mkdtemp(join(tmpdir(), "settings-conformance-"));
38
+ const dir = join(root, "config.d");
39
+ await mkdir(dir, { recursive: true });
40
+ const when = new Date(mtime);
41
+ try {
42
+ for (const [name, text] of Object.entries(files)) {
43
+ const p = join(dir, name);
44
+ // 上面那条单段律要保证的**就是**这条包含律。它是承重面上的断言,不是可跳过的装饰:真要有一天
45
+ // 单段律被改松,这里要 loud(清掉已建的临时根再抛),不许静默写出去。
46
+ if (!resolve(p).startsWith(resolve(dir) + sep)) {
47
+ throw new Error(`conformance: local-root file \`${name}\` resolves outside the case root — 语料不许写出自己的临时根`);
48
+ }
49
+ await writeFile(p, text, "utf8");
50
+ await utimes(p, when, when);
51
+ }
52
+ }
53
+ catch (e) {
54
+ await rm(root, { recursive: true, force: true });
55
+ throw e;
56
+ }
57
+ return { root, cleanup: () => rm(root, { recursive: true, force: true }) };
58
+ }
59
+ /**
60
+ * 语料落盘的**文件名闸**。语料是仓外贡献面(每条 case 的 `input.json` 里的 `localFiles` 键,以及
61
+ * {@link deriveLocalInput} 从 **domains 顶层键**派生的 `<域名>.json`),所以这里的名字不是自己写的常量:
62
+ * 一个 `"../../x.json"` 会顺着 `join(dir, name)` 写到临时根之外、覆盖宿主文件,而 `cleanup()` 只删临时根,
63
+ * 删不到跑出去的那一份。闸设在**落盘这个单铸点**上 —— 派生路、显式语料路、消费方自己调 kit 的路共一道闸,
64
+ * 零特判、零复述。错形沿用 kit 的 harness 错形(`conformance: …` 的 `Error`,经适配器腿记成 `harness-error`)。
65
+ */
66
+ function assertBareFileName(name) {
67
+ const bad = name.length === 0 ? "is empty"
68
+ : name === "." || name === ".." ? "is a directory reference"
69
+ : name.includes("\0") ? "contains a NUL byte"
70
+ : name.includes("/") || name.includes("\\") ? "contains a path separator"
71
+ : basename(name) !== name ? "is not a bare file name"
72
+ : undefined;
73
+ if (bad !== undefined) {
74
+ throw new Error(`conformance: local-root file name ${JSON.stringify(name)} ${bad} — 语料给的文件名必须是**单段文件名**(single path segment),落盘处不做路径拼接`);
75
+ }
76
+ }
77
+ /** 一条腿的入参:语料给了显式的就用显式的,否则按变换表从 `domains` 派生。 */
78
+ function legInput(c, leg) {
79
+ const { version, updatedAt } = c.input.meta;
80
+ switch (leg) {
81
+ case "domains":
82
+ return { leg, docs: c.input.domains, version, updatedAt };
83
+ case "wire":
84
+ return c.input.wirePayload === undefined ? deriveWireInput(c.input.domains, version, updatedAt) : { leg, payload: c.input.wirePayload };
85
+ case "localRoot":
86
+ return c.input.localFiles === undefined ? deriveLocalInput(c.input.domains, updatedAt) : { leg, files: c.input.localFiles, mtime: updatedAt };
87
+ default: {
88
+ const unhandled = leg;
89
+ throw new Error(`conformance: unknown leg ${String(unhandled)}`);
90
+ }
91
+ }
92
+ }
93
+ /** 磁盘形的腿期望 → 运行期形(`derive:true` 在这里真的派生)。`null` = 语料声明本腿不适用。
94
+ *
95
+ * **导出**给覆盖律用:覆盖矩阵要数的是「这条腿最终被期望成什么」,那必须是**驱动真正用的那一份**期望
96
+ * ——测试自己再算一遍派生,就是第二份真源,而两份真源的分歧正是本门要抓的病。 */
97
+ export function resolveLegExpectation(c, leg, baseline) {
98
+ const spec = c.expected[leg];
99
+ if ("applicable" in spec)
100
+ return null;
101
+ if ("derive" in spec) {
102
+ const source = c.expected.domains;
103
+ if (!("outcome" in source) || source.outcome !== "effective") {
104
+ throw new Error(`conformance corpus: ${c.id}/expected.json#${leg} asks to derive from a domains expectation that is not \`effective\` — 拒绝态不派生,那条腿要写显式期望`);
105
+ }
106
+ const from = { outcome: "effective", effective: source.effective, warnings: source.warnings, metadata: source.metadata };
107
+ if (leg === "wire") {
108
+ // **派生不许替被测 helper 同时造输入与期望**(codex R1 [high] 复现):skills 是五条变换里唯一一条
109
+ // **算出一个新值**的(content → contentHash),而入参与期望都走同一只 `toManifestDoc` —— 把哈希算错
110
+ // 也会两侧同样算错,45 条 wire 语料照样全绿。于是这里 fail-loud:凡该变换**不是恒等**的 case,
111
+ // wire 腿必须写显式期望(金样入库即冻结,哈希一改就红)。其余四条变换只搬/只删,不铸新值,照常派生。
112
+ if (stableKey(toManifestDoc(source.effective.skills)) !== stableKey(source.effective.skills)) {
113
+ throw new Error(`conformance corpus: ${c.id}/expected.json#wire cannot be derived — 这条 case 的 skills 表示变换会**算出新值**(contentHash),` +
114
+ `派生会让入参与期望同时错;请给 wire 腿写显式期望并写明 why(跑 \`npm run conformance:bless\` 填金样)`);
115
+ }
116
+ return deriveWireExpectation(from, c.input.domains);
117
+ }
118
+ if (leg === "localRoot")
119
+ return deriveLocalExpectation(from, c.input.domains, baseline, spec.localVersion);
120
+ throw new Error(`conformance corpus: ${c.id}/expected.json#${leg} cannot be derived`);
121
+ }
122
+ if (spec.outcome === "effective")
123
+ return { outcome: "effective", effective: spec.effective, warnings: spec.warnings, metadata: spec.metadata };
124
+ return { outcome: "rejected", stage: spec.stage, ...(spec.code === undefined ? {} : { code: spec.code }) };
125
+ }
126
+ /** effective 里的元数据两键(独立断言,不进结构比对)。 */
127
+ const METADATA_KEYS = ["version", "updatedAt"];
128
+ function splitMetadata(effective) {
129
+ if (effective === null || typeof effective !== "object" || Array.isArray(effective))
130
+ return { body: effective };
131
+ const src = effective;
132
+ // fromEntries(不是逐键赋值):`__proto__` 作自有键时逐键赋值会走原型 setter,把它从比对面上抹掉。
133
+ const body = Object.fromEntries(Object.entries(src).filter(([k]) => !METADATA_KEYS.includes(k)));
134
+ return { body, version: src.version, updatedAt: src.updatedAt };
135
+ }
136
+ /**
137
+ * 坐标 `patchPath` 是否落在登记的 `pathPrefix` **这棵子树**里 —— 判据是 **JSON Pointer 段前缀**,
138
+ * 不是裸串前缀:`/limits/infraCostRates` 盖 `/limits/infraCostRates` 与 `/limits/infraCostRates/**`,
139
+ * **不**盖 `/limits/infraCostRatesInjected`;`/models/1` 不盖 `/models/10`。裸 `startsWith` 会让一条登记
140
+ * 悄悄豁免没登记的相邻坐标,而账本是**逐坐标**白名单(棘轮),那就等于绕开它自己那条律。
141
+ * `prefix = ""` 表示根(release 模式另有大包大揽判据挡着,这里不特判)。
142
+ */
143
+ const underPointerPrefix = (patchPath, prefix) => patchPath === prefix || patchPath.startsWith(`${prefix}/`);
144
+ /**
145
+ * 一条 effective diff 的坐标落在账本哪一条上 —— 命中返回条目下标,没命中 `undefined`。
146
+ * 三道判据缺一不可:腿相同 ∧(`cases` 缺席或含该 case)∧ 坐标在 `pathPrefix` 那棵子树里。
147
+ */
148
+ function ledgerIndexFor(ledger, caseId, leg, patchPath) {
149
+ for (let i = 0; i < ledger.length; i++) {
150
+ const e = ledger[i];
151
+ if (e.outcome !== undefined)
152
+ continue; // 结局形登记不管坐标(见 ledgerIndexForOutcome)
153
+ if (e.leg !== leg)
154
+ continue;
155
+ if (e.cases !== undefined && !e.cases.includes(caseId))
156
+ continue;
157
+ if (!underPointerPrefix(patchPath, e.pathPrefix))
158
+ continue;
159
+ return i;
160
+ }
161
+ return undefined;
162
+ }
163
+ /**
164
+ * 一条**结局形**分歧落在账本哪一条上。四道判据全等,一道都不放宽:腿 ∧ case 在点名名单里 ∧
165
+ * 金样期望的结局 ∧ 消费方实际的结局。没命中 `undefined`。
166
+ */
167
+ function ledgerIndexForOutcome(ledger, caseId, leg, shape) {
168
+ for (let i = 0; i < ledger.length; i++) {
169
+ const o = ledger[i].outcome;
170
+ if (o === undefined)
171
+ continue;
172
+ // 形不合法的登记(`cases` 缺席/空 —— 违例已单记)绝不因此**扩大**覆盖:它谁都不命中,fail-closed。
173
+ if (o.leg !== leg || !Array.isArray(o.cases) || !o.cases.includes(caseId))
174
+ continue;
175
+ if (o.expected !== shape.expected || o.actual !== shape.actual)
176
+ continue;
177
+ return i;
178
+ }
179
+ return undefined;
180
+ }
181
+ /** 结局形不一致的一格:附上**结构化**的 `diff.outcome`,命中账本就记 `diff-ledgered`。 */
182
+ function outcomeVerdict(base, detail, shape, ledger, hits) {
183
+ const diff = { effective: [], warnings: { missing: [], extra: [] }, metadata: [], outcome: shape };
184
+ const i = ledgerIndexForOutcome(ledger, base.caseId, base.leg, shape);
185
+ if (i === undefined)
186
+ return { ...base, status: "diff", detail, diff };
187
+ hits[i] += 1;
188
+ return { ...base, status: "diff-ledgered", detail: `${detail} — 落在适配器的已知分歧账本里(结局形,债)`, diff, ledgerRefs: [ledger[i].ref] };
189
+ }
190
+ /** 一格的比对。返回 status + detail(+ diff)。`hits` 是账本逐条的命中计数器 —— 由本函数就地累加
191
+ * (比对与计数走同一遍遍历;再扫一遍就是第二份判定,两份判定的分歧正是本门要抓的那类病)。 */
192
+ function judge(c, leg, expected, actual, normalizer, ledger, hits) {
193
+ const base = { caseId: c.id, leg };
194
+ if (expected.outcome === "rejected") {
195
+ if (actual.outcome !== "rejected") {
196
+ return outcomeVerdict(base, `expected a rejection at stage \`${expected.stage}\`, got an effective config`, { expected: "rejected", actual: "effective" }, ledger, hits);
197
+ }
198
+ if (actual.stage !== expected.stage) {
199
+ return outcomeVerdict(base, `expected rejection stage \`${expected.stage}\`, got \`${actual.stage}\` (${actual.diagnostics})`, { expected: "rejected", actual: "rejected", stage: actual.stage }, ledger, hits);
200
+ }
201
+ if (expected.code !== undefined && actual.code !== expected.code) {
202
+ return outcomeVerdict(base, `expected rejection code \`${expected.code}\`, got \`${String(actual.code)}\``, { expected: "rejected", actual: "rejected", stage: actual.stage }, ledger, hits);
203
+ }
204
+ return { ...base, status: "expected-rejected", detail: `rejected at \`${actual.stage}\` as expected` };
205
+ }
206
+ if (actual.outcome === "rejected") {
207
+ return outcomeVerdict(base, `expected an effective config, got a rejection at \`${actual.stage}\`: ${actual.diagnostics}`, { expected: "effective", actual: "rejected", stage: actual.stage }, ledger, hits);
208
+ }
209
+ // ① 秘密面先判:告警里带着语料埋的秘密,比读数对不对更严重。
210
+ const leaks = findSecretLeaks(actual.warnings, c.meta.secrets ?? []);
211
+ if (leaks.length > 0) {
212
+ return { ...base, status: "diff", detail: `warning surface leaked ${leaks.length} corpus secret(s) — 告警不许比它诊断的东西更危险`, diff: { effective: [], warnings: { missing: [], extra: leaks }, metadata: [] } };
213
+ }
214
+ // ② effective(剥元数据)
215
+ const split = splitMetadata(actual.effective);
216
+ const expectedBody = canonicalize(expected.effective);
217
+ const actualBody = canonicalize(split.body);
218
+ const effectivePatch = deepEqual(expectedBody, actualBody) ? [] : diffJson(expectedBody, actualBody);
219
+ // ③ 告警多重集
220
+ let actualProjections;
221
+ try {
222
+ actualProjections = actual.warnings.map((w) => projectWarning(w));
223
+ }
224
+ catch (e) {
225
+ return { ...base, status: "harness-error", detail: `a returned warning could not be projected: ${e instanceof Error ? e.message : String(e)}` };
226
+ }
227
+ const warningDiff = diffWarningMultiset(expected.warnings, actualProjections, normalizer);
228
+ // ④ 元数据独立断言
229
+ const actualVersion = actual.version ?? split.version;
230
+ const actualUpdatedAt = actual.updatedAt ?? split.updatedAt;
231
+ const metadataPatch = [];
232
+ if (expected.metadata.version !== undefined && actualVersion !== expected.metadata.version) {
233
+ metadataPatch.push({ op: "replace", path: "/version", expected: expected.metadata.version, actual: actualVersion });
234
+ }
235
+ else if (expected.metadata.version === undefined && !(typeof actualVersion === "number" && Number.isFinite(actualVersion))) {
236
+ metadataPatch.push({ op: "replace", path: "/version", expected: "a finite number", actual: actualVersion });
237
+ }
238
+ if (actualUpdatedAt !== expected.metadata.updatedAt)
239
+ metadataPatch.push({ op: "replace", path: "/updatedAt", expected: expected.metadata.updatedAt, actual: actualUpdatedAt });
240
+ if (effectivePatch.length === 0 && warningDiff.missing.length === 0 && warningDiff.extra.length === 0 && metadataPatch.length === 0) {
241
+ return { ...base, status: "pass", detail: "effective, warnings and metadata all match" };
242
+ }
243
+ const diff = { effective: effectivePatch, warnings: warningDiff, metadata: metadataPatch };
244
+ const detail = `${effectivePatch.length} effective diff(s), ${warningDiff.missing.length} missing / ${warningDiff.extra.length} extra warning(s), ${metadataPatch.length} metadata diff(s)`;
245
+ // ⑤ 账本(3.0.0 车 FT):**只收 effective 的差**,且**逐坐标**全覆盖才算入账。
246
+ // 告警与元数据这两面永不可入账 —— 它们是这道门的承重面(一条被「已知」掉的告警差,正是本门要抓的病);
247
+ // 一格里只要有一条坐标没登记,整格仍是 `diff`(账本是白名单,不是整格豁免)。
248
+ const ledgerable = effectivePatch.length > 0 && warningDiff.missing.length === 0 && warningDiff.extra.length === 0 && metadataPatch.length === 0;
249
+ if (ledgerable) {
250
+ const idx = effectivePatch.map((p) => ledgerIndexFor(ledger, c.id, leg, p.path));
251
+ if (idx.every((i) => i !== undefined)) {
252
+ for (const i of idx)
253
+ hits[i] += 1;
254
+ return { ...base, status: "diff-ledgered", detail: `${detail} — 全部落在适配器的已知分歧账本里`, diff, ledgerRefs: idx.map((i) => ledger[i].ref) };
255
+ }
256
+ }
257
+ return { ...base, status: "diff", detail, diff };
258
+ }
259
+ /** release 模式的**来源律 + 版本护栏**(historical 模式不跑这几条:它就是拿旧版包比行为的)。 */
260
+ async function releaseViolations(adapter, opts, corpus, kit) {
261
+ const v = [];
262
+ if (opts.only !== undefined)
263
+ v.push("release mode forbids `only` — 一个能挑 case 的发版门不是门");
264
+ if (opts.normalizer !== undefined)
265
+ v.push("release mode forbids a normalizer — 归一器只用来看旧版,不用来放宽今天的门");
266
+ if (corpus.version !== kit.version)
267
+ v.push(`corpus VERSION ${corpus.version} !== kit package version ${kit.version}`);
268
+ if (adapter.provenance.version !== corpus.version)
269
+ v.push(`adapter provenance version ${adapter.provenance.version} !== corpus VERSION ${corpus.version}`);
270
+ if (adapter.provenance.integrity.trim().length === 0)
271
+ v.push("adapter provenance has an empty `integrity` — 三仓装的是不是同一份 tarball,靠的就是这一行");
272
+ // provenance 不是一句自报:它指的那份 package.json 必须**真在磁盘上**、真是这个包、版号真与自报值相同。
273
+ // 只核「是不是绝对路径」等于没核 —— 一个指向不存在目录的路径照样全绿(codex R1 [high] 复现)。
274
+ const pj = adapter.provenance.packageJsonPath;
275
+ if (!isAbsolute(pj)) {
276
+ v.push(`adapter provenance packageJsonPath must be absolute, got \`${pj}\``);
277
+ }
278
+ else {
279
+ try {
280
+ const declared = JSON.parse(await readFile(pj, "utf8"));
281
+ if (declared.name !== kit.name)
282
+ v.push(`adapter provenance packageJsonPath is not the contract package: ${pj} declares name \`${String(declared.name)}\`, expected \`${kit.name}\``);
283
+ else if (declared.version !== adapter.provenance.version)
284
+ v.push(`adapter provenance packageJsonPath version mismatch: the file says \`${String(declared.version)}\`, the adapter reports \`${adapter.provenance.version}\``);
285
+ }
286
+ catch {
287
+ v.push(`adapter provenance packageJsonPath cannot be read as JSON: ${pj}`);
288
+ }
289
+ }
290
+ // 账本条目的**形**:一条大包大揽的 `pathPrefix`(`/` 或空串)不是账本,是一个 `ok=true` 开关;
291
+ // 没有 `ref` / `flipWhen` 的一条登记没法被人查、也没法到期翻转(棘轮靠这两个字段才立得住)。
292
+ for (const e of adapter.knownDivergences ?? []) {
293
+ if (e.ref.trim().length === 0)
294
+ v.push("a known divergence has an empty `ref` — 查不到出处的一条登记等于没登记");
295
+ if (e.flipWhen.trim().length === 0)
296
+ v.push(`known divergence \`${e.ref}\` has an empty \`flipWhen\` — 说不出什么时候删,它就永远不会被删`);
297
+ if (e.outcome === undefined) {
298
+ if (!e.pathPrefix.startsWith("/") || e.pathPrefix.length < 2)
299
+ v.push(`known divergence \`${e.ref}\` has a blanket \`pathPrefix\` (\`${e.pathPrefix}\`) — 账本要逐坐标点名,整片豁免不是账本`);
300
+ continue;
301
+ }
302
+ // 结局形那一臂的两条硬约束。类型上已经挡住(`kind` 只有 `"debt"`、`cases` 必填),但适配器可能来自
303
+ // JSON / `as` —— 门不靠类型自证,运行期照判。`kind` 先读进一个宽类型的局部,否则 TS 把「不是 debt」
304
+ // 那一支收成 `never`,连报错文案都取不出字段。
305
+ const kind = e.kind;
306
+ const cases = e.outcome.cases;
307
+ if (kind !== "debt")
308
+ v.push(`known divergence \`${e.ref}\` is an outcome divergence with kind \`${kind}\` — 结局形只能是 \`debt\`:契约说拒、消费方说不拒,不是「成文语义」盖得住的分歧`);
309
+ if (!Array.isArray(cases) || cases.length === 0)
310
+ v.push(`known divergence \`${e.ref}\` is an outcome divergence that must name at least one case — 结局形不许按前缀大包一整条腿的拒绝面`);
311
+ }
312
+ const legs = Object.entries(adapter.legs).filter(([, l]) => l !== undefined);
313
+ if (legs.length === 0)
314
+ v.push("adapter registers no leg at all");
315
+ for (const [name, leg] of legs) {
316
+ const mod = leg.entry.module;
317
+ if (!isAbsolute(mod)) {
318
+ v.push(`leg \`${name}\` entry.module must be an absolute path, got \`${mod}\``);
319
+ continue;
320
+ }
321
+ if (!MODULE_EXTENSIONS.includes(extname(mod)))
322
+ v.push(`leg \`${name}\` entry.module is not a module file (${mod}) — 入口得是个模块,不是随便一个存在的文件`);
323
+ let real = mod;
324
+ try {
325
+ real = await realpath(mod); // 先解软链再判:一条指向 node_modules 里契约包的软链不算「在外面」
326
+ const s = await stat(real);
327
+ if (!s.isFile())
328
+ v.push(`leg \`${name}\` entry.module is not a file: ${mod}`);
329
+ }
330
+ catch {
331
+ v.push(`leg \`${name}\` entry.module does not exist: ${mod}`);
332
+ }
333
+ const insideContract = contractInNodeModules(kit.name);
334
+ if (mod.includes(insideContract) || real.includes(insideContract))
335
+ v.push(`leg \`${name}\` entry.module sits inside the contract package (${mod}) — 那条腿证明的是「包等于包」`);
336
+ if (leg.entry.export.trim().length === 0)
337
+ v.push(`leg \`${name}\` entry.export is empty`);
338
+ }
339
+ return v;
340
+ }
341
+ /**
342
+ * 跑一遍门。
343
+ *
344
+ * `release`:版本三方对齐(适配器加载到的包 == 语料 VERSION == kit 所在包)+ 每条登记的腿都指向一个
345
+ * **真存在、且不在契约包 node_modules 之下**的生产入口 + 禁 `only` + 至少登记一条腿 + **登记的腿必须跑**
346
+ * (未登记的腿只记 `skipped: no adapter`,不算违例)+ 账本条目的形 + 零陈腐账本条目。
347
+ * `historical`:对旧版包跑,只记行为 diff;归一器可在场且必须自陈有损。
348
+ */
349
+ export async function runConformance(adapter, opts) {
350
+ const corpus = await loadCorpus(opts.corpusDir);
351
+ const kit = await kitIdentity();
352
+ const violations = opts.mode === "release" ? await releaseViolations(adapter, opts, corpus, kit) : [];
353
+ const only = opts.only === undefined ? undefined : new Set(opts.only);
354
+ const cases = only === undefined ? corpus.cases : corpus.cases.filter((c) => only.has(c.id));
355
+ if (only !== undefined) {
356
+ const missing = [...only].filter((id) => !corpus.cases.some((c) => c.id === id));
357
+ if (missing.length > 0)
358
+ throw new Error(`conformance: \`only\` names unknown case id(s): ${missing.join(", ")}`);
359
+ }
360
+ const ledger = adapter.knownDivergences ?? [];
361
+ const hits = ledger.map(() => 0);
362
+ const results = [];
363
+ for (const c of cases) {
364
+ for (const leg of LEG_NAMES) {
365
+ const expected = resolveLegExpectation(c, leg, corpus.baseline);
366
+ if (expected === null) {
367
+ const spec = c.expected[leg];
368
+ results.push({ caseId: c.id, leg, status: "skipped", detail: `case declares this leg not applicable: ${spec.why}` });
369
+ continue;
370
+ }
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" });
377
+ continue;
378
+ }
379
+ let actual;
380
+ try {
381
+ actual = await impl.run(legInput(c, leg));
382
+ }
383
+ catch (e) {
384
+ 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)}` });
385
+ continue;
386
+ }
387
+ results.push(judge(c, leg, expected, actual, opts.normalizer, ledger, hits));
388
+ }
389
+ }
390
+ const summary = { pass: 0, diff: 0, "diff-ledgered": 0, "expected-rejected": 0, "harness-error": 0, skipped: 0 };
391
+ for (const r of results)
392
+ summary[r.status] += 1;
393
+ const ledgerRows = ledger.map((e, i) => e.outcome === undefined
394
+ ? { 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
+ : { 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
+ if (opts.mode === "release") {
397
+ // **登记的腿必须跑**:登记了却在整套语料上一格真读数都没有,那条登记是空的。
398
+ for (const leg of LEG_NAMES) {
399
+ if (adapter.legs[leg] === undefined)
400
+ continue;
401
+ if (!results.some((r) => r.leg === leg && r.status !== "skipped")) {
402
+ violations.push(`leg \`${leg}\` is registered yet never ran — 整套语料上一格真读数都没有(登记的腿必须跑)`);
403
+ }
404
+ }
405
+ // **棘轮只降不升**:账本里一条从未命中的登记,自己就是违例 —— 债还了就得把账本删掉。
406
+ for (const row of ledgerRows) {
407
+ if (row.hits === 0)
408
+ violations.push(`stale ledger entry \`${row.ref}\` (leg \`${row.leg}\`, ${row.outcome === undefined ? `pathPrefix \`${row.pathPrefix}\`` : `outcome ${row.outcome.expected}→${row.outcome.actual}`}) never matched a diff — 差异没了就把这一条删掉(${row.flipWhen})`);
409
+ }
410
+ }
411
+ const uniqueViolations = [...new Set(violations)];
412
+ return {
413
+ mode: opts.mode,
414
+ adapter: { name: adapter.name, provenance: adapter.provenance, legs: LEG_NAMES.filter((l) => adapter.legs[l] !== undefined) },
415
+ corpusVersion: corpus.version,
416
+ kitVersion: kit.version,
417
+ ...(opts.normalizer === undefined ? {} : { normalizer: { id: opts.normalizer.id, lossy: opts.normalizer.lossy, why: opts.normalizer.why } }),
418
+ results,
419
+ summary,
420
+ ledger: ledgerRows,
421
+ violations: uniqueViolations,
422
+ ok: uniqueViolations.length === 0 && summary.diff === 0 && summary["harness-error"] === 0,
423
+ };
424
+ }