@sema-agent/settings-schema 13.0.0 → 13.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.
@@ -0,0 +1,42 @@
1
+ # CC 偏离登记(本包)
2
+
3
+ 本包镜像或对齐 Claude Code(CC)上游的设置语义时,**有意**不照抄的地方逐条登在这里:差在哪(CC 原形与语料坐标 / 本包的形)、哪个方向、为什么、谁读它。
4
+ 锚 = CC 2.1.296 语料 `cli296.js`(只 grep,不解包;坐标写行号,行号属于那一份语料,只引不抄)。
5
+
6
+ 方向只有两种写法:**更严**(本包拦下 / 不放行 / 不装 CC 会放行或会装的)与**更宽**(本包拦下 CC 不拦的 —— 只出现在拒绝方向)。
7
+ 本包不登「比 CC 更松」的偏离;出现了即是缺陷。
8
+
9
+ ## hooks classifier(`classifyHooksConfig`,`src/hooks-config.ts`)
10
+
11
+ 七处都只在**更严**的方向上:CC 会装上的文档,这里可能整份不装;反过来的情形不存在。代价都是同一个 —— 一份本可装上的设置文件整份不装(同文件的 `permissions.deny` 一起不生效),所以每条都写了它挡的是什么。
12
+
13
+ | # | 本包 | CC 2.1.296 | 为什么 |
14
+ |---|---|---|---|
15
+ | H1 | 守卫迹象扫**整棵子树**,每个顶层键都看 | 只看 3 层(看不到的当没有),并跳过 `env` / `mcpServers` / `pluginConfigs` 等九个顶层键(`:237470` `$xe` / `J8` / `Qr`) | 第 4 层起放一个守卫、或塞进被跳过的键下,CC 照装且守卫无声消失;cli 修车实测可绕 |
16
+ | H2 | 多带一个事件名键的匹配器形对象照算迹象 | `npe` 把「不带 `matcher`、带事件名键」的对象当事件表、不算匹配器 | 多写一个键就能让一只匹配器躲过判定 |
17
+ | H3 | 条目自己的字段也扫(含 `headers` / `input` 里以守卫事件名为键的成员) | 遇到带字符串 `type` 的对象就不往里看 | 带 `type` 不证明里面没藏东西;本会被剥掉的字段里的守卫会无声消失 |
18
+ | H4 | 已知非守卫事件的值写成一只匹配器(不是数组)⇒ 整份拒 | 剔掉那个键(警告) | 匹配器形本身就是迹象(规则一条,不因事件名分档) |
19
+ | H5 | 整份文档本身写成一只匹配器(顶层带 `matcher` 键 + 匹配器形)⇒ 整份拒 | 无此判 | 把一只守卫匹配器误贴成整份设置时,CC 剔掉坏的 `hooks` 后照装 |
20
+ | H6 | `hooks` 是装着**数组**的数组 ⇒ 整份拒(与装对象同判) | 只对装对象 / 装迹象的数组整份拒,装数组的剔掉 | 套一层数组的守卫不该被当成标量剔掉 |
21
+ | H7 | 条目字段以本包字段表为准(`HOOK_ENTRY_FIELDS`);CC 独有字段(`args` / `onFailure` / `cloud` / `continueOnBlock` / `rewakeMessage` …)不进装上的那一份,也不算坏块 | 认得这些字段 | 契约只收本包定义的字段;字段级的上游漂移今天没有门(type 集有,见漂移门 ①b) —— 未闭环 |
22
+
23
+ 不是偏差的(与 CC 同):守卫事件集 {`PreToolUse`, `PermissionRequest`};守卫事件值 `null` = 剔不拒;非守卫事件里坏匹配器 / 坏条目逐个剔;hook 条目 type 五种。
24
+
25
+ ## URL 名单与 http hook 名单(`src/url-pattern.ts` / `src/http-hook-allowlists.ts`)
26
+
27
+ | 号 | 面 | CC 原形 | 本包 | 方向 | 理由 | 读它的 |
28
+ |---|---|---|---|---|---|---|
29
+ | SS-CCD-01 | URL 名单放行出口(`urlAllowPatternMatches`) | `g9`(249195):模式写明协议缺省端口(`https://*.x:443/*`)解析后端口读成空,主机带 `*` ⇒ 端口不判 = 放行任意端口 | 写明的端口照判(「写没写」由解析器换同族协议再解析答) | 更严 | 管理员写了 `:443` 就是只许 443 | http hook URL 名单、MCP 允许名单 `serverUrl` |
30
+ | SS-CCD-02 | 同上 | 模式协议整段 `*` 通配一切目标协议,含 `ws+unix:` 等本机套接字协议 | `*` 协议只通配 http / https / ws / wss;别的协议须逐字写出;`ws+unix:` 的套接字段只认逐字(带 `*` 不放行任何套接字),空套接字段 / 含 `..` 段的目标不放行;没写路径的模式不放行套接字 | 更严 | 这四种的连接目标就是主机与端口;套接字路径到连接层还会被改写,glob 判不住真实目标 | 同上 |
31
+ | SS-CCD-03 | 同上 | 模式解析不出 URL ⇒ 回落按 `/` 分段 glob | 放行出口不回落,不命中 | 更严 | 回落把主机与端口拼成一段比,`*` 能跨过两者边界(`http://*:1*/*` 吞进 `[::1]:9000`) | 同上 |
32
+ | SS-CCD-04 | 同上 | 主机名里 `%2a` / 全角 `*` 被解析器解码成字面 `*`,随后被当通配 | 解析后主机名里不是占位来的 `*` ⇒ 不放行 | 更严 | 那是字面字符,不是管理员写的通配 | 同上 |
33
+ | SS-CCD-05 | URL 名单拒绝出口(`urlDenyPatternMatches`) | 只按 `g9` 组件判 | 组件判 ∪ 整串 glob | 更宽(拒绝方向) | 既有无协议 / 跨组件的拒绝写法(`*blocked.example*`)不因换判法失效 | MCP 拒绝名单 `serverUrl` |
34
+ | SS-CCD-06 | http hook 名单跨层(`httpHookUrlVerdict` / `httpHookEnvVarsAllowed`) | 各层**拼接**(237460「Arrays merge across settings sources (same semantics as allowedMcpServers)」) | 每一张准入的名单都须放行(取交) | 更严 | 拼接 ⇒ 下层可往托管名单里加 URL = 绕过管理员 | 壳 http hook 执行器与请求体出门筛;server 引擎属事件 hook |
35
+ | SS-CCD-07 | http hook 名单准入层 | 任意层可设 | 只认准入层:`layerAdmission(源, 工作区读数, "httpHookAllowlists") === "admitted"`(源表 `SETTINGS_SOURCE_CONTRACT` 定档、类目地板 `global` ⇒ 用户 / flag / 托管层;项目 / 本地层在地板之下,工作区受不受信都一样) | 更严 | 仓库可控层的一个 `[]` 会让用户自己的 http 守卫 hook 不发出 = 守卫失效(放行方向),与 `sandboxNetwork.failIfUnavailable` 同一条边界 | 同上 |
36
+
37
+ 实现差(不是行为偏离,登在这里免得被当成偏离):URL 判官的占位记号上游用每进程随机串,本包按输入确定选取(纯函数、零随机);
38
+ 选取律保证记号不出现在模式与目标的任何解析读法里,也不与相邻字面拼出第二份。
39
+
40
+ ## settings sources(`SETTINGS_SOURCE_CONTRACT`,`src/settings-sources.ts`)
41
+
42
+ 无偏差:本地层与项目层同为「仓库可控」档(`:237757`);`flagSettings` 归运维档 `global`(与 user / policy 并列)。
@@ -0,0 +1,344 @@
1
+ # 跨仓一致性门 —— `@sema-agent/settings-schema/conformance`
2
+
3
+ > 契约仓自带**金样语料 + 比对器**;每个消费方把**自己的生产入口**塞给 kit 跑一遍 ⇒ 同一份 settings
4
+ > 语料在 web-admin、worker、管理面、契约仓自身读出的 effective 逐字节一致、告警多重集一致;写入席对同一份
5
+ > 文档**收不收**也一致(8.0.0 第四条腿 `strictWrite`)。3.0.0 起随包发。
6
+
7
+ ## 0. 为什么要这道门
8
+
9
+ 2.0.0 / 2.1.0 两版的正确性靠「表态 + 黑盒测试 + 各仓提货时各自红先」——三样都是**单仓视角**。真正的
10
+ 跨仓分歧一直是被提货过程**碰巧**撞出来的。碰巧不是门。
11
+
12
+ 写侧(管理面)判断哪条坏、整域退不退,读侧(worker / CLI)逐条容错,此前没有任何一格把两侧放在同一份
13
+ 文档上对过。3.0.0 又恰好改铸了告警坐标 —— 最容易让几方读法悄悄分叉的一版。
14
+
15
+ ## 1. 三步接线(消费方侧)
16
+
17
+ **四条腿,每条都要表态**(8.0.0,一条规则、四腿同律):实现它,或显式声明不承担。`legs` 是全量
18
+ `Record<LegName, LegStance>` —— 闭集加一条腿,每个消费方的适配器就编译红,直到它对新腿表态;JS / `as` 来的适配器
19
+ release 门照判(缺表态 / 形不对 = 违例)。**不表态不是一种表态**:没有「静默跳过」,也没有「默认当通过」。
20
+
21
+ | 腿 | 它比什么 | 契约仓的生产入口 |
22
+ |---|---|---|
23
+ | `domains` | 各域原始文档(web-admin 存储形)→ effective(读:逐条抢救 + 降级 + 告警) | `buildEffective` |
24
+ | `strictWrite` | 同一份各域原始文档 → 写入席**收不收**(严写受理,§1.1) | `parseDomainLoud` + `danglingRefs` |
25
+ | `wire` | `GET /effective` 载荷 → effective | `readEffectiveWire` |
26
+ | `localRoot` | `config.d/` 目录快照 → effective | `loadLocalConfig` |
27
+
28
+ 声明不承担 = `{ declined: <理由词>, why: <一句事实> }`。两臂**互斥**(类型层:实现臂 `declined?: never`、声明臂 `entry?: never` / `run?: never`;
29
+ release 门在运行期同判):从「声明」迁到「实现」时 spread 合并留下旧声明,编译期就红。收窄一条表态用 `stance.declined !== undefined`。理由词是闭集 `LEG_DECLINE_REASONS`,今天一个词:
30
+ `no-production-path` —— 你的生产代码里**结构上就没有**这种读法 / 写法(worker 只读不写 ⇒ 没有严写受理;worker 不从各域
31
+ 原始文档建 effective ⇒ 没有 `domains` 腿;管理面不读 `config.d/` ⇒ 没有 `localRoot` 腿)。`why` 非空。声明的腿逐格记
32
+ `skipped`(detail = `consumer declines this leg (<理由词>): <why>`),报告 `adapter.declined` 逐条列出 —— 看得见、不算通过、
33
+ 也不算红。刻意**没有**「有这条路径、还没接」的词:有路径就接;结局上的已知差走 §9 账本(带台账号、带翻转条件、受 stale 律管)。
34
+
35
+ 两种消费方各自怎么接:
36
+
37
+ - **只读的 worker(server)**:实现 `wire` + `localRoot`;`domains` 与 `strictWrite` 声明
38
+ `{ declined: "no-production-path", why: "worker 只读不写:写入只经 web-admin;effective 来自 wire 与 config.d/,本仓无 buildEffective 调用点" }`
39
+ (`why` 写你自己仓的事实)。
40
+ - **写入席(管理面 / web-admin 产品)**:实现 `domains`(存储 → effective 的那条读法)+ `strictWrite`(域写受理那一段:
41
+ 对文档集里**每一个**键跑你的写受理,任一份不受理 ⇒ 返回 `{ outcome: "rejected", stage: "validate", diagnostics }`;全部受理 ⇒
42
+ `{ outcome: "effective", effective: { <键>: <受理后的域文档> }, warnings: [], version: input.version, updatedAt: input.updatedAt }`);
43
+ 不读 `/effective` 载荷、不读 `config.d/` 的就把 `wire` / `localRoot` 声明不承担。
44
+
45
+ ```ts
46
+ import { runConformance, type ConformanceAdapter } from "@sema-agent/settings-schema/conformance";
47
+ import { fileURLToPath } from "node:url";
48
+ import { createRequire } from "node:module";
49
+
50
+ const require = createRequire(import.meta.url);
51
+ // `./package.json` 是包的**资产出口**(3.0.0 起在 exports 表里登记)—— exports 表一旦在场就挡住所有
52
+ // 没登记的子路径,所以这一行能跑通本身是契约的一部分,有一格门钉着它。
53
+ const pkgPath = require.resolve("@sema-agent/settings-schema/package.json");
54
+
55
+ const adapter: ConformanceAdapter = {
56
+ name: "server", // 语料 contributor 同词表
57
+ provenance: { // 你**实际加载到**的那份契约包
58
+ packageJsonPath: pkgPath,
59
+ version: require(pkgPath).version,
60
+ integrity: "sha256:…", // 预发 tarball 的 sha256 / npm 的 integrity
61
+ },
62
+ legs: {
63
+ // 四条腿逐条表态。没有的腿**不要造**,声明它(理由词闭集 + 一句事实);律指向你**实现了的腿**:
64
+ // ①至少实现一条腿;②实现的腿必须跑(实现了却一格真读数都没有 ⇒ 违例)。
65
+ domains: { declined: "no-production-path", why: "worker 不从各域原始文档建 effective(本仓无 buildEffective 调用点)" },
66
+ strictWrite: { declined: "no-production-path", why: "worker 只读不写:写入只经 web-admin" },
67
+ wire: { entry: { module: fileURLToPath(new URL("../src/config-center/http-client.ts", import.meta.url)), export: "fetchEffective" }, run: myWireLeg },
68
+ localRoot: {
69
+ entry: { module: fileURLToPath(new URL("../src/config-center/local-provider.ts", import.meta.url)), export: "fetchEffective" },
70
+ async run(input) {
71
+ if (input.leg !== "localRoot") throw new Error("wrong leg");
72
+ const { root, cleanup } = await materializeLocalRoot(input.files, input.mtime); // kit 单铸,别自己写落盘
73
+ try {
74
+ const { effective, warnings } = await myProductionLocalRead(root);
75
+ return { outcome: "effective", effective, warnings };
76
+ } catch (e) {
77
+ return { outcome: "rejected", stage: "parse", diagnostics: String(e) }; // 拒绝是结局,不是异常
78
+ } finally {
79
+ await cleanup();
80
+ }
81
+ },
82
+ },
83
+ },
84
+ };
85
+
86
+ const report = await runConformance(adapter, { mode: "release" });
87
+ expect(report.violations).toEqual([]);
88
+ expect(report.ok).toBe(true);
89
+ ```
90
+
91
+ 不绿就读报告:每格的 `diff` 把 **effective 的 JSON patch**、**告警多重集差**(`missing` / `extra`)、
92
+ **元数据差**三面分开列,不必再靠肉眼比两坨 JSON。
93
+
94
+ ### 1.1 严写腿 `strictWrite`(8.0.0)
95
+
96
+ - **入参**与 `domains` 腿**同一份**(`DomainDocsInput`:各域原始文档 + `version` + `updatedAt`,恒等,不派生)。
97
+ - **结局**只用既有两类,写入席两道:① 逐域 `parseDomainLoud` —— 任何一份不受理 ⇒ `rejected{ stage: "validate" }`(写是一个动作,
98
+ 不是逐份抢救;`parseDomainLoud` 是文档面读函数,永不抢救 —— 12.3.0 起逐条抢救只在视图面 `parseDomainSalvaged` 里);② 整份受理后的文档集过一次**那一只引用判官** `danglingRefs`(没有的域 = 空;正反两向一次判完,
99
+ 不逐域各问一遍 —— 那样同一条悬空引用会被报两次)—— 有悬空引用 ⇒ `rejected{ stage: "validate", code: "ref-integrity" }`(`REF_INTEGRITY_REJECT_CODE`),拒体带悬空引用坐标的
100
+ 多重集 `refIssues`,金样同存同比(坐标不同 ⇒ diff;句子不比)。都过 ⇒ `effective` = 文档集里每个键 → 受理后的域文档(**只含提交的键,
101
+ 不补缺席域**;空集受理成 `{}`),告警面 = ① 铸的剥键 / 承运告警(11.0.0 起不再恒空),元数据回报 harness 注入的那一份。
102
+ - **律**(金样逐条核,`test/conformance-strict-write-8.0.0.test.ts`):严写**拒** ⇔ 文档集里有不是域名的键 ∨ 读腿整条拒 ∨
103
+ 读腿在提交的某个域上抢救过(`entry-dropped` / `domain-defaulted` / `hosts-grandfathered`);否则严写**受理**,受理形 =
104
+ 读腿 effective 里那几格,告警面 = 读腿在提交域上铸的剥键 / 承运告警(两腿同一只判据铸);另有悬空引用一臂(律只推 roster → 模型目录
105
+ 这一类,今天语料里只有这一类)。读腿只剥键 / 只承运的地方严写照样受理 —— 那是解析自己的语义,不是抢救。
106
+ 今天 90 条语料:39 受理、51 拒(读腿抢救过的 26 条;读腿整条拒的 17 条;带非域名键的 7 条;悬空引用的 1 条)。
107
+ - **为什么不派生**:严写结局不是读结局的一种表示变换(读腿抢救处严写拒);从 `domains` 期望派生会让一份金样的错同时改写两条腿。
108
+ 每条 case 的严写金样显式入库、各自冻结,`why` 写在金样旁边。
109
+ - **负控**(`test/conformance-mutation.test.ts` 第十一靶):把 `parseDomainLoud` 换成读腿的宽松解析 ⇒ 严写腿**恰好**红在读腿抢救过的
110
+ 那 26 条上;把非域名键静默跳过 ⇒ 恰好红在那 7 条上。
111
+
112
+ ## 2. 报告形
113
+
114
+ | 结局 | 意思 |
115
+ |---|---|
116
+ | `pass` | effective、告警多重集、元数据三面全对 |
117
+ | `expected-rejected` | 这条 case 期望这条腿**拒绝**,而它确实在期望的那一层拒了(绿) |
118
+ | `diff` | 读数与期望不符(effective / 告警 / 元数据任一面) |
119
+ | `harness-error` | 适配器**抛了**而不是返回一个结局 —— 接线坏了,不是契约坏了 |
120
+ | `diff-ledgered` | 这一格的 effective 差**逐坐标**落在适配器的已知分歧账本里(见 §9)—— 不阻塞,单独计数 |
121
+ | `skipped` | 没跑:这条 case 声明本腿不适用(带 `why`);消费方**声明不承担**这条腿(detail 带理由词与 `why`);或消费方**没表态** / 表态形不对(release 模式另记违例)。`skipped` 本身不算红 |
122
+
123
+ 「期望拒绝」与「harness 失败」刻意分开报:两者混在一起,就再也分不出「契约按设计拒收」与「消费方接线
124
+ 坏了」。
125
+
126
+ ## 3. 两种模式
127
+
128
+ - **`release`** —— 发版门。版本三方对齐(适配器加载到的包 == `conformance/VERSION` == kit 所在包)、
129
+ **每条腿都有表态**(实现,或 `{ declined, why }`;缺 / 形不对 = 违例)、每条实现了的腿都指向一个**真存在且不在契约包
130
+ 自己身上**的生产入口、禁 `only`、禁归一器、**至少实现一条腿**、**实现的腿必须跑**、账本条目的形合法且**零陈腐条目**(§9)。
131
+ - **`historical`** —— 对旧版包跑,只记行为 diff。可挂一只**自陈有损**的归一器(旧版坐标是点号串、今天是
132
+ 段数组)。`npm run conformance:historical` 就是它,产出进 `conformance/historical/`。归一器可带一只
133
+ **投影级**钩子 `warning?(p)`(先于 `path` 作用,期望与实际两侧同过):旧版**铸不出**的 additive 字段在这里
134
+ 剥掉 —— 今天是 `entry-dropped.identity`(S-712)与 `entry-dropped.reasonCode`(S-1295)—— 否则「旧版没有这个字段」会被记成「旧版行为红」。
135
+
136
+ ## 4. 四律(缺一不算「门」)
137
+
138
+ 1. **对抗律** —— 每条语料 `meta.claim` 非空(写清它想打谁),`meta.firstRedOn` **只能由 historical 模式
139
+ 真跑写回**,不许手填。护栏(`firstRedOn` 为空)也收,但**计数分开**,不许混进深度读数。
140
+ 门:`test/conformance-corpus.test.ts`。
141
+ 2. **变异律** —— 对读路径每个铸点各注入一处变异(esbuild 真打包 → 产物文本**恰一处**替换 → 真 import),
142
+ 语料**必红**;活着的变异 = 语料缺口 = 门红。第九、第十个靶打在 **kit 自己身上**:比对器被改瞎却照样绿
143
+ = 门是假的;派生用的内容寻址被算错却照样绿 = 门对「内容寻址整片失效」那类错全盲。
144
+ 门:`test/conformance-mutation.test.ts`。
145
+ 3. **覆盖律** —— 按**消费方 × 腿 × 域 × 结局**计数;每个 `DomainName` ≥ 2 条、每个
146
+ `EffectiveReadWarning['kind']` ≥ 1 条(**穷举 switch**,新 kind 不表态就编译红)、
147
+ `DOMAIN_READ_FALLBACK` 两档各 ≥ 1 条(整域降级 / 整域拒)、`DOMAIN_ENTRY_SALVAGE` 两档各 ≥ 1 条(逐条丢 / 闸域整域拒;12.3.0)、
148
+ 登记的每条表示变换都真被某条语料触发。
149
+ 门:`test/conformance-coverage.test.ts`。
150
+ 4. **来源律** —— `release` 模式核 provenance 与每条腿的生产入口:
151
+ - `provenance.packageJsonPath` 必须**真在磁盘上**、真是这个包(`name` 相等)、`version` 与自报值相等
152
+ —— 只核「是不是绝对路径」等于没核(一个指向不存在目录的路径照样全绿,对抗复审复现)。
153
+ - `entry.module` 必须是**绝对路径**、**模块文件**(`.ts`/`.js`/`.mjs`/… —— `package.json` 不是入口)、
154
+ 真存在,且(解软链后)**不落在契约包自己身上**。
155
+ **「契约包自己」是一只判别式,两形闭集**(4.0.0;单铸在 `src/conformance/entry-origin.ts`):
156
+ `own-build-output` = 路径落在**正在跑这段码的那份**契约包的 `<包根>/dist/` 下(拿自己比自己);
157
+ `another-installed-copy` = 路径落在**另一份**装好的契约包(`node_modules/<包名>/`)里。
158
+ 两形**先判「我自己」**:消费方装好的那份 kit 自己就住在 `node_modules` 里,它的 dist 同时命中两形,
159
+ 顺序反了最该拒的那一形就会被放行。咬 `dist/` 而不是整个包根,是因为契约仓**自证**时腿的入口是本仓的
160
+ `src/*.ts`(那对契约仓而言就是生产入口),而 `src/` 不随包发。
161
+ **判之前先归一**:两侧都 `resolve`(点段 / 双斜杠)再尽力 `realpath`(软链)—— 判据咬的是**文件**,
162
+ 不是字符串;`historical` 模式不跑这道门,工厂那一侧是唯一的闸,所以归一做在判别式里。
163
+ **两处执法,同一只判别式;哪一形归哪一处有判据**(4.0.0):`createContractAdapter` 看不见 `mode`,
164
+ 所以它只拒**在任何模式下都错**的那一形 —— `own-build-output`(**当场拒**,抛,不是告警);
165
+ `another-installed-copy` 在 `historical` 下是**正当**的(那个模式就是拿旧版包比行为,而旧版包常常正好
166
+ 装在 `node_modules/<包名>/` 布局下)⇒ 工厂不拒,留给看得见 mode 的 `release` 门。手写的适配器字面量
167
+ (§2 的接线配方)走不到工厂 ⇒ 那道门**两形都判**。拒绝句与违例句同源,都说清「你落在哪儿 /
168
+ 合法的是哪儿」。
169
+ **它的诚实边界**:kit **不能**证明那个文件内部没有直接转调契约函数 —— 它没法加载消费方的 TypeScript
170
+ 源、也不该去猜。那一半由**消费方半场的变异自证**负责:消费方对自己的折叠 / 投影层注入一处变异,
171
+ kit 必红。本律是护栏,不是证明。
172
+
173
+ ## 5. 语料目录布局
174
+
175
+ ```
176
+ conformance/
177
+ VERSION # = 包版本;release 模式三方对齐的那一头
178
+ baseline.json # 空文档集的 effective(冻结 golden;非便携域的那一格由它供给)
179
+ historical/<包>@<版本>.json # historical 真跑证据(firstRedOn 的唯一出处)
180
+ cases/<id>/
181
+ input.json # { meta:{version,updatedAt}, domains:{…}, wirePayload?, localFiles? }
182
+ expected.json # 每条腿一格(四条一条不少):显式 golden / {derive:true}(只 wire / localRoot)/ {applicable:false,why}
183
+ meta.json # { contributor, claim, secrets?, firstRedOn, note? }
184
+ migrate-workers/<id>/ # 7.0.0:workers 域一次性迁移的冻结金样(不是读腿语料,kit 不跑它)
185
+ input.json # 原始 6.x workers 文档 { workers:[…] }(`meta.inputFrom` 在场 ⇒ 由加冕脚本取那条的输出生成)
186
+ expected.json # migrateWorkersDomain(input) = { doc, changed, dropped, refused }(refused[].original = 输入那一行逐字)
187
+ meta.json # { contributor, claim, inputFrom? }
188
+ ```
189
+
190
+ - **迁移金样**(7.0.0 起,`m01–m05`:G5 正臂 / G5 幂等 / G5 refused / G7b / 8.2.0 逐行终判 —— 没动的行在 `env` 里设了保留名)给接 `migrateWorkersDomain` 的消费方(web-admin 的 workers 读口)
191
+ 对拍用:把 `input.json` 喂进你的读口,`doc` / `changed` / `dropped` / `refused` 应逐字等于 `expected.json`,再对 `doc` 跑一次应零变化。
192
+ 本仓 `test/conformance-workers-7.0.0.test.ts` 钉着这三件;`npm run conformance:bless`(全量)同时加冕它们。迁移窗口关闭时整个目录随迁移代码删除。
193
+
194
+ - **真源是 `domains`**(各域原始文档)。`wire` / `local-root` 两腿的**入参与期望**都由变换表派生;
195
+ 派生解释不了的腿写显式期望并带 `why`。`strictWrite` 的入参与真源恒等、期望恒为显式金样(§6 的「期望从哪来」表)。
196
+ - 加一条语料:写 `input.json` + `meta.json` + `expected.json` 骨架(哪条腿派生 / 哪条腿显式;`strictWrite` 写
197
+ `{"why": "<为什么受理 / 为什么拒>"}`),跑 `npm run conformance:bless` 填金样,跑 `npm run conformance:historical` 填 `firstRedOn`。
198
+ - `contributor` **只写真交了的仓**。替别人写的语料带不进那个仓的读法,只会让门看起来更满。
199
+
200
+ **test 的四条(S-570,2026-09-22)**:`t01-absent-domain-key-not-null` / `t02-entry-salvage-vs-whole-domain-refusal`
201
+ / `t03-prototype-name-key-not-swallowed` / `t04-secret-literal-never-in-warning-value` —— 来源
202
+ `sema-test` 仓 `repro/ss300-corpus-20260919/cases/`(该围栏对 3.0.0 的黑盒验收批,`contributor` 字段
203
+ 在源头就已是 `"test"`);本仓只读取用、原样收编,未改写 `claim`/`note` 的措辞。`firstRedOn` 未沿用
204
+ sema-test 的读数,是本仓自己重跑 `node scripts/conformance-historical.mjs 1.0.0 1.5.0 1.10.0 2.0.0 2.1.0`
205
+ 对五个历史发布版真实产出的(与源头读数逐字一致,交叉印证)。四条把 `contributor: "test"` 从 0 条
206
+ 带到 4 条(`test/conformance-corpus.test.ts` 对抗律①的机器记账 pin 已同批更新)。
207
+
208
+ ## 6. 四条腿的期望从哪来 + 读腿之间的表示差异(登记表,`src/conformance/derive.ts`)
209
+
210
+ **期望从哪来**是一张闭集表 `LEG_EXPECTATION`(`Record<LegName, …>`,新开一条腿不表态即编译红);语料加载器只读这张表判
211
+ 「这条腿能不能写 `{derive:true}`」,不按腿名特判:
212
+
213
+ | 腿 | 期望 | 为什么 |
214
+ |---|---|---|
215
+ | `domains` | 显式 | 真源:它的期望就是金样本身 |
216
+ | `strictWrite` | 显式 | 入参与 domains 恒等,期望不派生(§1.1) |
217
+ | `wire` | 可派生 | T1 / T2 / T4 |
218
+ | `localRoot` | 可派生 | T3 / T4 / T5 / T6 |
219
+
220
+ 三条**读**腿之间的表示差异(严写腿不在这张表里 —— 它与 domains 的差不是表示变换):
221
+
222
+ | id | 影响的腿 | 动了什么 |
223
+ |---|---|---|
224
+ | `T1-prompts-materialisation` | wire | `buildEffective` 恒物化 `prompts:{bindings:[]}`;wire 按**在场性** verbatim 挂回(缺席进缺席出,且在场时是**未解析的原文档**) |
225
+ | `T2-skills-representation` | wire | 域形带 `content`,wire 形带 `contentHash`(manifest);判据 schema 随表示走,于是 wire 腿**没有** skills 的剥键告警 |
226
+ | `T3-non-portable-domains` | local-root | `config.d/` 只读 `PORTABLE_DOMAINS`;其余域取 schema 默认 + `unread-config-file{non-portable-domain}`。**由常量派生,不手抄域名** |
227
+ | `T4-unknown-top-level-key` | wire + local-root | wire 是 open-world,不认识的顶层键**逐字承运**;`domains` 腿按域名取文档故无声;local-root 报 `unread-config-file{not-a-domain}` |
228
+ | `T5-file-layer-first` | local-root | `config.d/<域>.json` 必须是 JSON **对象**(空文件 / `null` / 数组 / 标量一律 fail-closed),这一层在域解析**之前** |
229
+ | `T6-file-name-classification` | local-root | `config.d/` 的文件分三类:域文件(读)/ `NON_DOMAIN_CONFIG_FILES`(`remote-exec.json` —— 有自己的读取与校验路径,**不**报未知文件)/ 隐藏文件与非 `.json`(扫描忽略);只有剩下的才报 `not-a-domain` |
230
+
231
+ 凡不能由这张表解释的差异,一定显示成 `diff` —— 于是「有没有第七条变换没登记」不再靠人回忆。
232
+
233
+ **派生不许替被测 helper 同时造输入与期望**:T2 是六条里唯一**算出一个新值**的(`content` → `contentHash`)。
234
+ 若入参与期望都走同一只哈希函数,把哈希算错会**两侧同样错**,整套 wire 语料照样全绿(一次真实的双向抵消
235
+ 假绿,对抗复审复现)。所以凡该变换**非恒等**的 case,kit **fail-loud 拒绝派生** wire 腿 —— 那条腿必须写
236
+ 显式金样(入库即冻结,哈希一改就红),另有一格变异律压着这只手。其余五条变换只搬 / 只删,不铸新值,照常派生。
237
+
238
+ ## 7. 规范化与比对(kit 内单铸,消费方零复述)
239
+
240
+ - **canonical JSON**:对象键排序、数组保序、`undefined` 值的键剔掉。建对象走 `Object.fromEntries`,
241
+ **不是**逐键赋值 —— 后者对 `"__proto__"` 走原型 setter,会把一个叫 `__proto__` 的顶层键从比对面上
242
+ 悄悄抹掉(两边同样被抹 = 比对器全盲 = 一格假绿)。
243
+ - **告警投影**:对 `EffectiveReadWarning` 联合做**穷举 typed 投影** = 每臂减掉 `error`,**从联合逐臂派生**
244
+ (4.0.0:属主在契约包的 `./config-fns`,kit 只再导出 —— 此前是 kit 里一份**手写**字段表,给某个 kind 加
245
+ 一个字段两边都不红);`error` 文案**不比对**(它可经脱敏、可随 zod 改字,比文案 = 比实现),但先断言它
246
+ **不含语料自带的秘密**。`entry-dropped` 的 `identity`(S-712:被丢条目原文里读得出的 `name` / `id` /
247
+ `enabled`,读不出 ⇒ 键缺席)**在**投影里 —— 它是契约铸的数据,不是文案;一条自己重铸告警、漏带身份的读腿
248
+ 会在告警面报 `missing` / `extra`;**消费方自己入库的语料**(例:web-admin `test/conformance/cases/wa05-*` 在
249
+ 被丢的 skills 条目上断言 `entry-dropped`)提货时要按新读数重新加冕 —— additive 字段让旧金样少一个键,
250
+ 是提货成本,不是读法分歧。`reasonCode`(S-1295:为什么丢的机读闭集词)同理在投影里 —— 语料里 `entry-key-invalid` /
251
+ `entry-schema-invalid` 由 JSON 表达得出;`entry-unreadable`(判的过程抛了)需要病态访问器,JSON 表达不出,由
252
+ `test/dropped-entry-reason-code-s1295.test.ts` 看着;web-admin 自有语料同样要在提货时按新读数重新加冕。
253
+ - **多重集**而不是集合:同一 kind 在同一域上发两次是有信息的,去重会把「报了两条」读成「报了一条」。
254
+ - 一条告警**内部**的集合字段(`keys` / `droppedNames`)同样按**袋**比:它们的顺序来自文档枚举次序,契约
255
+ 从没承诺过 —— 照序列比,消费方只要换个扫描次序就会在读数完全相同的情况下把发版门顶红。重数仍然保留;
256
+ 而一条 `ConfigPath` **内部**的段序当然仍是序列(`["a","b"]` 与 `["b","a"]` 是两条不同的坐标)。
257
+ - **坐标按结构比**:段数组进 JSON 编码,`["a.b"]` 与 `["a","b"]`、`["1"]` 与 `[1]` 都分得开。
258
+ kit 内**零** `formatConfigPath` 调用 —— 出门形是给人看的,不是比对用的。
259
+ - **元数据独立断言**:`version` / `updatedAt` 不进结构比对(harness 注入 / 本地腿按内容哈希与 mtime 自算),
260
+ 单列一面。
261
+
262
+ ## 8. 秘密扫描的口径(如实登记)
263
+
264
+ 扫的是**告警**(投影 + `error` 文案),**不是** effective。effective 里逐字承运运维自己写下的那份文档是
265
+ 契约(配置就是他写的那份);而告警是本包**自己铸的新文本** —— 一条把秘密抄进坐标或错误文案的告警,会把
266
+ 秘密带进日志、审计流、管理面,那才是「告警比它诊断的东西更危险」。
267
+
268
+ 扫的是一条腿交回的**每一处新文本**:读出时的告警(投影 + `error` 文案)、拒绝时的诊断句(`diagnostics`)。而且**先于一切**判
269
+ (11.0.0):带着语料秘密的一格不论结局形对不对、账本盖不盖得住,都是 `diff`、不可入账 —— 此前「期望拒、实际读出」这类结局分歧在
270
+ 判秘密之前就早返回,那条腿确实交回了告警,泄漏却被跳过。
271
+
272
+ ## 9. 已知分歧账本(`knownDivergences`)—— 给**消费方自己**的债用
273
+
274
+ 消费方对金样的差异过去只能写在测试注里,而散文不是判据:没人说得清「这条差异还在不在」。
275
+ 账本把它变成机器可核的一行 —— 而且**只收债**(11.0.0 删了「成文语义」`declared`:它没有到期,一行登记活过了它指向的票):
276
+
277
+ ```ts
278
+ const adapter: ConformanceAdapter = {
279
+ // …
280
+ knownDivergences: [
281
+ { leg: "wire", pathPrefix: "/limits/infraCostRates", kind: "debt",
282
+ ref: "A-075.41", window: "2026-10-31", flipWhen: "本仓改完 limits 投影后删掉这一条" },
283
+ ],
284
+ };
285
+ // release 模式挂着账本 ⇒ 调用方传钟:runConformance(adapter, { mode: "release", nowMs: Date.now() })
286
+ ```
287
+
288
+ - **`kind`**:只有 `"debt"`。**`ref`** 是**台账票号**(`LEDGER_REF_RULE`:`ABC-12` / `ABC-12②` / `AB-C3` / `XY-7.3` 这一形,
289
+ 没有空白)—— 一段散文查不到出处,违例。**`window`** 是这笔债最晚活到哪天(`YYYY-MM-DD`,UTC 日历日,当天仍在窗内);
290
+ release 模式下钟(`nowMs`)晚于它 ⇒ 违例(还债,或在票上带理由挪窗)。库码不读环境时钟:账本非空而没传 `nowMs` ⇒ 违例
291
+ (账本为空不需要钟)。
292
+ - **命中判据**三条缺一不可:`leg` 相同 ∧(`cases` 缺席或含该 case)∧ diff 的 JSON Pointer 落在
293
+ `pathPrefix` 这棵子树里。第三条是 **JSON Pointer 段前缀**,不是裸串前缀:坐标 `=== pathPrefix`,
294
+ 或以 `pathPrefix + "/"` 开头。于是登记 `/limits/infraCostRates` **不**顺带豁免
295
+ `/limits/infraCostRatesInjected`,登记 `/models/1` **不**顺带豁免 `/models/10` —— 账本是逐坐标白名单,
296
+ 一条登记悄悄盖住相邻坐标就等于绕开它自己那条律。命中的格记 `diff-ledgered`,**不阻塞**;报告的
297
+ `ledger` 按 `ref` 列出每条的命中数。
298
+ - **只收 effective 的差**:**告警差与元数据差永不可入账**。它们是这道门的承重面 —— 一条被「已知」掉的
299
+ 告警差,正是本门要抓的那类病。
300
+ - **逐坐标白名单,不是整格豁免**:同一格里多一条没登记的坐标,整格仍然是 `diff`。
301
+ - **棘轮:只降不升**。账本里一条**从未命中**任何 diff ⇒ 违例 `stale ledger entry` —— 债还了就得把账本
302
+ 删掉,否则它自己把门顶红。`pathPrefix` 必须点名至少一个顶层键(`/` 或空串是一个 `ok=true` 开关,
303
+ 不是账本),`ref` 是票号、`window` 是日历日且未过、`flipWhen` 非空(查不到出处、说不出何时删的一条登记等于没登记)—— 都是违例。
304
+ ### 9.1 结局形分歧
305
+
306
+ 有一类差根本没有 effective patch:**契约期望这条腿拒绝,消费方却读出了 effective**(降级 + 诊断),
307
+ 或反过来。比对停在「结局形就不一样」那一步,坐标形账本按 `pathPrefix` 段前缀够不着它 —— 而它恰恰是门最该
308
+ 点名、也最该逼着修的一类。所以账本有第二条臂:
309
+
310
+ ```ts
311
+ { kind: "debt",
312
+ outcome: { leg: "wire", cases: ["c17-empty-domain-file"], expected: "rejected", actual: "effective" },
313
+ ref: "A-075.NN", window: "2026-10-31", flipWhen: "本仓改成按契约拒收后删掉这一条" }
314
+ ```
315
+
316
+ - 命中判据**四道全等**:腿 ∧ case 在点名名单里 ∧ 金样期望的结局 ∧ 消费方实际的结局。
317
+ - **`cases` 必填且非空** —— 结局形分歧不许按前缀大包一整条腿的拒绝面;形不合法的一条登记(违例已单记)
318
+ **谁都不命中**(fail-closed:坏形绝不因此扩大覆盖)。
319
+ - 与坐标形同律:是债,带票号、窗与翻转条件;写成别的 `kind` 是违例。类型上已经挡住,运行期照判(适配器可能来自 JSON / `as`,
320
+ 门不靠类型自证)。
321
+ - 入账后与坐标形**同一张账本、同一套 stale 律**:记 `diff-ledgered`、同一计数、同一 `ledger` 行,
322
+ 一条从未命中仍是违例。
323
+ - 报告里这类格的 `diff.outcome = { expected, actual, stage? }` 是**结构化**的 —— 靠 `detail` 文案分辨
324
+ 结局形的差等于没有判据(下游真出过「`[].every` 把拒绝结局整片放过」那一类空洞)。
325
+
326
+ - **契约仓自证不许用**:`test/conformance-self.test.ts` 的适配器**不挂账本**。契约仓是金样的作者,
327
+ 给自己开豁免等于改金样而不改金样文件 —— 那不是「已知分歧」,那是把门关了。
328
+
329
+ ## 10. 未闭环(如实)
330
+
331
+ - **严写腿没有 historical 读数**:`conformance/historical/` 的五份旧版真跑证据是三腿时代(3.0.1)跑的,只覆盖当时的 53 条
332
+ case × 三条读腿;严写腿上旧版 `parseDomain` 与今天的差(例如旧版不认识的键被剥掉)要重跑 `npm run conformance:historical`
333
+ 才看得见,而重跑会改写一批 `firstRedOn`,8.0.0 没有做。
334
+ - **严写受理 runtime 旧文档时三闸旧键被静默剥掉**(语料 c06 / c20 的严写金样是 🚧 特征格):读腿从 runtime 旧文档抬升 governance
335
+ 三闸;写入席不抬升,`runtime` schema 剥掉三闸旧键(不拒、不告警 —— 读腿的剥键扫描本就豁免这三个键)。写入席若按受理形落库,
336
+ 读侧抬升此后找不到它们。若裁「写侧拒 runtime 上的三闸旧键」,这两格翻 `rejected{validate}`。
337
+
338
+ - **消费方半场的 `firstRedOn` 测不到**:`firstRedOn` 只能反映**契约包自身**在旧版上的行为差,消费方
339
+ (server / 管理面)在它们 2.1.0 期的读腿分歧,要等它们交 adapter 才量得到。
340
+ - **有损归一器会抹平真差异**:对 2.1.0 跑时,S-323c 的坐标改铸(刻意的契约变更)被归一器抹平 —— 同一套
341
+ 抹平也会掩盖「两条不同的段坐标拼成同一个串」这类真二义。归一器自陈 `lossy:true` 就是为了让这句话
342
+ 必须被说出来,而不是被当成「没差别」。S-712 起它还两侧剥掉 `entry-dropped.identity`(旧版铸不出):
343
+ 身份的差在 historical 读数里同样看不见;S-1295 起 `entry-dropped.reasonCode` 同理。
344
+ - **来源律不能证明非同义反复**(见 §4-4):那一半归消费方半场的变异自证。