Skip to content

spec 双源清账 C9:RateLimitConfig / RateLimitConfigSchema(./integration ≠ ./shared)—— 2 条 #4684

Description

@os-zhuang

#4535 的 C9 簇,v17 收口三簇之一。基线行:

RateLimitConfig       — [./integration (type)]  ≠ [./shared (type)]
RateLimitConfigSchema — [./integration (const)] ≠ [./shared (const)]

基线现为 19 条,本簇目标 19 → 17。

本簇在作者面上 —— tombstone 风险是真的

静态引用图确认 RateLimitConfigSchema 从 BUILTIN_METADATA_TYPE_SCHEMAS 可达。已知嵌入点(开工请自行复核):

侧 嵌入点 可作者化 key
./shared shared/http.zod.ts:144 声明;api/endpoint.zod.ts:47 与 api/registry.zod.ts:379 的 rateLimit: 3
./integration integration/connector.zod.ts:336 声明 6

任何可作者化 key 消失都会真的静默剥离作者写的值(schema 非 .strict(),Zod 直接吞)。

C8 是最接近的先例(RetryPolicy,同样两侧都活、都可达):它把单一声明放进 shared/retry-policy.zod.ts、由两个入口 re-export,利用「发布的 def key 由入口命名空间决定」这一点,让两个 def key 都存活且 key 集相同 —— 因此只付了 1 个 key 的代价而不是 8 个。本簇很可能适用同一手法(注意 ./shared 侧的声明已经在 shared/http.zod.ts,情况可能更简单)。

纪律(#4535 §1–§4 + 手册 6/7/8,开工前必读)

  1. 默认路线:收敛 + re-export,优先选让两侧 key 并集存活的方向。 不删 key 就不需要 tombstone。若两侧存在「同一概念两种拼法」(C8 的 retryDelayMs vs backoffMs),并集保全不可行 —— 同时声明两者是 alias 反模式,必须选一个并对被丢的键走完整 ADR-0087。
  2. ⛔ 禁止手编 packages/spec/authorable-surface.json(authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650)。gen:schema 只允许因新增 key 而重写它。
  3. ⚠️ 门禁绿 ≠ 登记正确(build-schemas.ts 检查 (b) 用叶名匹配 conversion surface —— 无关簇的 .type 就能让一个 tombstone 冒充「已登记迁移」 #4659):检查 (b) 按 leaf name 匹配 conversion surface,无关 conversion 也能满足。自己枚举 ALL_CONVERSIONS 的 clause,确认以你 retire 的那个叶名结尾的全仓只有 1 条且就是你的(C8 的做法)。
  4. ⚠️ 默认值 / 约束的变更不被任何门禁记录(spec 门禁盲区:可作者化 key 的「默认值 / 约束」变更不被任何 gate、tombstone 或 conversion 记录(#4650 / #4659 同族) #4666)。 若收敛导致某个 .default() 或 .min()/.max() 改变,必须用手写运行时测试钉住,并在 changeset 里显式写明;若存量文档会因此改变行为,用 conversion 显式物化旧值(C8 的做法:把 pre-17 默认值写进每个省略了它的存量文档,已部署栈行为不变)。
  5. 回归 pin 用运行时模块命名空间断言(packages/spec 的「编译期 pin」是失效的:tsconfig 排除了 *.test.ts,vitest 也不做类型检查 #4642 已证编译期 pin 空转),必须 sabotage 验证并贴输出。
  6. 判不出来就升级,不要猜(spec 双源清账 C5:ActivationEventSchema(./kernel ≠ ./studio)—— 1 条 #4653 / spec 双源清账 C6:EventSchema(./automation ≠ ./kernel)—— 1 条 #4658 / spec 双源清账 C8:RetryPolicy / RetryPolicySchema(./automation ≠ ./system)—— 2 条 #4661 先例)。分析本身就是交付物。
  7. ⚠️ 生成物冲突只能靠重新生成(手册第 7 条,2026-08-02 实测教训)。main 在 v17 收尾期前进很快,你跑完门禁大概率已落后。推之前重新 git fetch origin main 判一次;要合并时:字符串数组可集合合并,spec-changes.json 是对象数组,必须跑 gen:spec-changes(集合合并会丢条目,已把 CI 打红过)。pnpm install --filter @objectstack/spec... 走缓存约 2.5 秒,生成器秒级。解完冲突务必本地跑对应 check:* 再推。
  8. ⛔ 不要碰 content/docs/releases/。

验收

  • 基线删掉上面 2 行,19 → 17,只减不增。
  • 全绿:build、check:dual-source-exports、check:generated、test,加源码审计组(check:liveness / check:strictness-ledger / check:empty-state / check:variant-docs / check:exported-any / check:skill-examples),以及全仓 pnpm typecheck。
  • 删 zod 形状则同步 docs/audits/2026-07-unknown-key-strictness-ledger.md。
  • changeset 一份,@objectstack/spec major,含 FROM → TO 与迁移指引。
  • 注意 spec 现为 17.0.0-rc.1、pre-mode 仍开;major 通道在 changeset pre exit 关闭。

关联:#4535(主单)、#4661 / PR #4670(最接近的先例)、#4650 / #4659 / #4666(门禁洞)、#4642(pin 空转)、#4675(生成物冲突)、ADR-0049、ADR-0087、ADR-0104

Activity

  1. self-assigned this
    on Aug 2, 2026
  2. removed their assignment
    on Aug 2, 2026
  3. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    📋 状态:已立单,未开工,未认领 —— 可直接领取

    本单由 #4535 的协调会话创建,但没有派发任何 agent,没有写任何代码,没有建任何分支或 worktree。维护者在派发前叫停,改由其他 agent 接手。

    已按仓规解除 assign,以免「已认领」的假象让人绕开它(所有 agent 共用一个 GitHub 身份,assignee 字段无法区分是谁的认领 —— 见 CLAUDE.md)。

    接手方式

    1. 按仓规先 assign 自己,并留 claim 评论(含你的 session ID 与分支名 claude/issue-4684-rate-limit-config-dual-source);
    2. 开工前重读本单评论 —— 若出现比你更早、session ID 不同的 claim,说明已被领走;
    3. worktree-first,然后按上面正文的纪律实施。

    接手前必读


    Generated by Claude Code

  4. self-assigned this
    on Aug 2, 2026
  5. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 1 轮(#4535 v17 串行三簇之第 1 簇)
    会话:session_0176qgxgCXTJCUv4YFLtusP9
    分支:claude/issue-4684-rate-limit-config-dual-source
    Worktree:objectstack-issue-4684

    派发前已核对的前置

    项 结果
    时效窗口 ✅ .changeset/pre.json 仍是 mode: pre / tag: rc,packages/spec 为 17.0.0-rc.1 —— major 通道未关
    基线现状 ✅ origin/main 的 dual-source-exports.baseline.json 实测 19 条,本簇两行(RateLimitConfig / RateLimitConfigSchema)均在列
    认领竞争 ✅ 本单此前仅有协调会话「未认领、可直接领」一条评论,无更早的其他 session claim
    串行纪律 ✅ 本轮只派本簇一个 agent;C11 / C12 待本簇 PR 合并后再立单

    ⚠️ 派发时 main 上的在途 spec PR(接手 agent 请注意)

    派发时刻有 3 个未合并的 spec PR 在途,都会动同一批生成物:

    它们与本簇源文件不重叠(本簇在 shared/http.zod.ts / integration/connector.zod.ts),但生成物必然重叠。按 #4535 处置手册第 7 条执行:推之前重新 git fetch origin main 判一次;spec-changes.json 是对象数组,冲突只能跑 gen:spec-changes,不可集合合并;解完冲突务必本地跑对应 check:* 再推。


    Generated by Claude Code

  6. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    🛑 停下来升级裁决(#4535 处置手册 §6)—— 本簇不是 C8 形状,是 C6 形状

    会话:session_0176qgxgCXTJCUv4YFLtusP9,分支 claude/issue-4684-rate-limit-config-dual-source(未推,零提交)。

    正文的假设「本簇很可能适用 C8 同一手法」经复核不成立。两侧不是「同一概念两种拼法」,是两个不同的概念;而仓规为「两个概念」准备的那条路线(§2 的「改名一侧」)在本簇被门禁堵死,且没有任何可诚实满足的补救。三条可走的路线各自的代价都落在维护者的协议意图上,按 §6 停下来,分析如下。


    一、两侧是两个概念,不是两种拼法

    shared/http.zod.ts:127 integration/connector.zod.ts:300
    方向 入站 —— 限别人调我们的 API 出站 —— 限我们调外部系统
    嵌入点 apis[].rateLimit、ApiEndpointRegistration.rateLimit、httpServer.security.rateLimit connectors[].rateLimitConfig
    时间窗 windowMs(毫秒),default 60000 windowSeconds(秒),必填,min 1
    配额 maxRequests int,default 100 maxRequests min 1,必填
    开关 enabled bool,default false 无 —— 写了就是开
    算法 无(隐含固定窗口) strategy 四值枚举,default token_bucket
    上游感知 无 respectUpstreamLimits(default true)+ rateLimitHeaders {remaining, limit, reset}

    关键在最后两行:respectUpstreamLimits / rateLimitHeaders 讲的是「读取上游返回的 429 头」—— 入站时我们就是上游,这两个键在 apis[].rateLimit 上结构性无意义;反过来 enabled: false 的默认值在 connector 上是反的(声明了 rateLimitConfig 显然是想开)。

    实测:沉默剥离风险是真的,而且今天就在发生

    shared.parse({})                                  => {"enabled":false,"windowMs":60000,"maxRequests":100}
    integration.safeParse({}).success                 => false  (maxRequests / windowSeconds 均 invalid_type)
    integration.parse({maxRequests:10,windowSeconds:60})
                                                      => {"strategy":"token_bucket","maxRequests":10,"windowSeconds":60,"respectUpstreamLimits":true}
    shared.parse({windowSeconds:60, strategy:'token_bucket'})
                                                      => {"enabled":false,"windowMs":60000,"maxRequests":100}
    

    最后一行就是 ADR-0104 的沉默剥离类:按 connector 的写法喂给 shared 的形状,两个键被静默吞掉、解析干净返回。同一个名字、两种必填性、两种单位 —— 这正是 #4411 陷阱最坏的版本。

    可达性复核 —— 与正文写的路径不同,但结论相同

    正文说「静态引用图确认从 BUILTIN_METADATA_TYPE_SCHEMAS 可达」。我用自验过的追踪器(自测:FieldSchema 命中 field、shared/retry-policy.zod.ts 的 RetryPolicySchema 命中 job,与 C8 的结论一致)重跑,两侧都不从那 24 个根可达:connector 与 api 都不在 BUILTIN_METADATA_TYPE_SCHEMAS 里。

    真实路径是 §1(b) 说的另一扇门:stack.zod.ts:266 的 apis: → ApiEndpointSchema.rateLimit,stack.zod.ts:395 的 connectors: → DeclarativeConnectorEntrySchema → ConnectorSchema.rateLimitConfig。两者都在 PLURAL_TO_SINGULAR 里登记为元数据类型。所以「作者能真写、丢键会真丢」的结论成立,只是理由要改。


    二、门禁实测:改名路线被堵死,且无法诚实补救

    Sabotage 1 —— 把 integration 侧改名 ConnectorRateLimitConfig

    先撞 manifest ratchet(可按提示删 manifest 行,是被允许的「deliberate removal」),删掉后撞到检查 (a):

    ❌ 6 authorable key(s) disappeared from the contract:
         - integration/RateLimitConfig:burstCapacity
         - integration/RateLimitConfig:maxRequests
         - integration/RateLimitConfig:rateLimitHeaders
         - integration/RateLimitConfig:respectUpstreamLimits
         - integration/RateLimitConfig:strategy
         - integration/RateLimitConfig:windowSeconds
    

    注意这 6 个键一个也没有真的从作者面消失:改名后 connectors[].rateLimitConfig.windowSeconds 逐字节照旧解析。变的只是 def key 这个内部 schema 名。§3 说的「改名不绕开 tombstone」实测复现。

    而门禁给出的补救在结构上不可能执行:

    • 提示的第 1 步是 retiredKey() —— 但 tombstone 需要一个活着的 def 来承载那个 z.never() 键;改名恰恰就是把 integration/RateLimitConfig 这个 def 拿掉。没有地方放墓碑。
    • 第 2 步是登记 D2 conversion —— 但 src/conversions/registry.ts 的 surface 全是作者路径(flow.node.config.filter、object.compactLayout,41 条无一例外),而 def 改名没有任何作者路径发生变化。要登记就只能编一条假的迁移,告诉消费者去改写根本不需要改写的元数据 —— C5 已经明确拒绝过这种做法(「conversion walker 经验证不可达,故写手工迁移而非伪造 conversion」)。
    • 手编 authorable-surface.json 把 6 行改名 —— 被 authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650 / 纪律 §2 明令禁止。

    一句话:ratchet 的计量单位是 def key,conversion 登记表的计量单位是作者路径;schema 改名正好是「在后者里无从表达、在前者里致命」的那一类变更。

    Sabotage 2 —— C8 的 re-export 机制在本簇确实成立

    把 integration 侧换成 re-export shared 的声明后:

      ✓ integration/RateLimitConfig.json          ← def key 存活
    ❌ 5 authorable key(s) disappeared from the contract:
         - integration/RateLimitConfig:burstCapacity / rateLimitHeaders / respectUpstreamLimits / strategy / windowSeconds
    

    即:收敛路线下两个 def key 都活,消失的只是合并形状里没有的键 —— 而且此时 tombstone 有地方放(挂在合并后的单一声明上,两个 def key 都会显示 [RETIRED])。C8 的机制在本簇技术上完全可用。问题不在能不能,在该不该。

    (两次 sabotage 均已回滚,工作树 git status 干净。)


    三、三条路线 · 双轴分析

    路线 A —— 改名 integration 侧为 ConnectorRateLimitConfig(ADR-0112 D9a 的既有裁决)

    D9a 原文:「packages/spec exports two mutually incompatible ErrorCategory and RetryStrategy types(api/errors.zod.ts vs integration/connector.zod.ts)—— the connector-side pair gets renamed so one name means one thing」。ConnectorRetryStrategySchema 就在本簇争议的 RateLimitConfigSchema 下方 5 行,注释里直接引了 D9a。#4535 §2 的第三条路线、基线文件自己的表头(「converge …, or rename one side」)说的都是这条。

    路线 B —— 收敛为两侧键集的完整并集(windowMs 与 windowSeconds 都留)

    • 门禁:今天就能全绿。 零 vanish、零 tombstone、零 conversion,gen:schema 只写入「新增」(纪律 §2 允许的唯一情形)。这是三条路线里唯一今天零阻力的。
    • 长期正确性:最差。 一个 schema 里同时声明 windowMs 和 windowSeconds,没有任何规则说谁赢 —— 这就是 PD#12 明令禁止的 alias 反模式被固化进 spec 本体(比消费者里的 ?? 更难拆:?? 只是 debt,写进 schema 就成了契约)。另外 4 个出站专用键会出现在 apis[].rateLimit 上,1 个入站专用键出现在 connector 上,凭空扩大「declared ≠ enforced」面(PD#10)。
    • 让 AI 写错的概率:最高。 两个单位不同、语义相同的键并排放着,AI 会两个都写、写矛盾的值(windowMs: 60000, windowSeconds: 30),而没有任何一层会报错。这条路线的全绿是门禁盲区的产物,不是正确性的证据。

    路线 C —— 收敛为「选定形状」(C8 的做法:留 windowMs,退休 windowSeconds)

    • 门禁:可走通,代价 1 个键。 windowSeconds 挂 retiredKey(),配一条 D2 conversion connector.rateLimitConfig.windowSeconds → windowMs(值要 ×1000,是单位换算不是改名)+ D3 链步。
    • 但还有两个门禁看不见的形状决定(spec 门禁盲区:可作者化 key 的「默认值 / 约束」变更不被任何 gate、tombstone 或 conversion 记录(#4650 / #4659 同族) #4666 盲区),都必须维护者定:
      1. maxRequests:shared 是 default(100)、integration 是必填。合并取默认值 ⇒ connector 漏写从「报错」变成「静默 100」(沉默的放松);合并取必填 ⇒ apis[].rateLimit: {} 从合法变非法(响亮的破坏,但 endpoint.test.ts:28 这类现有用法会当场断)。
      2. enabled:合并后 connector 凭空获得一个 default false 的开关。今天没人读所以是潜伏的;一旦将来接上执行,每个存量 connector 的限流都是关的。C8 式的显式物化 conversion 能兜,但那是在为一个本不该存在的键写迁移。
    • 长期正确性:低。 即使解决了上面两点,结果仍然是把两个概念焊成一个 schema,四个出站专用键永久留在入站作者面上。C6(Event)对这种情形的判词可以原样搬过来:「两侧键集完全不相交 ⇒ 收敛等于写假话」。

    两轴冲突吗?

    不冲突。 两轴都指向 A:长期最干净,且是唯一在作者时刻用名字挡住错误的方案。B/C 的吸引力全部来自「今天门禁能过」,而 §二 已经证明门禁在这个维度上是瞎的(def key vs 作者路径的计量错配)。用门禁的盲区来选协议形状,正是 #4666 / #4650 这一串洞造成的最贵的一种误导。


    四、我的推荐

    路线 A + 给 ratchet 加声明式 def 改名承接表,在同一个 PR 里。承接表是十几行、可评审、严格强于现状(它要求旧 def 的每个键在新 def 下都存在,而今天手编基线可以无声删任何一行)。

    若维护者判断 v17 收尾期不宜动门禁代码,那么第二推荐是 本簇按 C6 的先例顺延 v18,与 #4650 的窄例外一起做 —— 而不是用 B 或 C 在 v17 内凑掉这 2 行基线。理由:基线数字是账本不是目标,用「把两个概念焊死」换 19 → 17,是拿一个永久的协议缺陷换一个临时的计数好看。C6 当初正是这么判的。

    ⛔ 明确不推荐 B。 它是唯一今天零阻力的路线,也是唯一会把 alias 反模式写进契约的路线。

    五、我做了什么 / 没做什么

    裁决后我可以直接接着做 —— A / B / C 三条的落地清单我都已经摸清了。


    Generated by Claude Code

  7. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    PM 复核:形状结论成立,且先例比报告说的更直接 —— 但待裁决的不是路线

    会话 session_0176qgxgCXTJCUv4YFLtusP9。dev agent 返回 needs_decision,零提交、零推送、worktree 已清理(已核:无远端分支、无残留 worktree)。以下是我对其结论的独立复核。

    一、两侧确实是两个概念(复核通过)

    shared/http.zod.ts        enabled(default false) · windowMs(ms, default 60000) · maxRequests(default 100)
                              ⇒ 三个键都有默认值,{} 可解析
    
    integration/connector.zod.ts
                              strategy(default token_bucket) · maxRequests(必填 min 1) · windowSeconds(秒, 必填 min 1)
                              · burstCapacity(可选) · respectUpstreamLimits(default true)
                              · rateLimitHeaders{remaining,limit,reset}(读上游 429 响应头的头名)
    

    respectUpstreamLimits 与 rateLimitHeaders 在入站侧结构性无意义 —— 服务端不会去读自己发出的 429 头。这不是「两种拼法」,是出站节流 vs 入站限流。windowMs(毫秒/有默认)与 windowSeconds(秒/必填)是同概念异单位;maxRequests 一侧有默认一侧必填。

    二、ADR-0112 D9(a) 是同一对文件、同一类冲突的既有裁决

    报告引作 "D9a",实际是 ADR-0112 的 D9 (a),原文:

    packages/spec exports two mutually incompatible ErrorCategory and RetryStrategy types (api/errors.zod.ts vs integration/connector.zod.ts) — the connector-side pair gets renamed (ConnectorErrorCategory, ConnectorRetryStrategy) so one name means one thing

    即:完全相同的两个文件、完全相同的「同名不同概念」形状,裁决是改 connector 侧的名。产物就在争议 schema 下方十行(connector.zod.ts:346 ConnectorRetryStrategySchema),注释里写着「The two value sets are not interchangeable, so they may not share a name (ADR-0112 D9a)」。

    三、因此路线本身不需要再裁决

    维护者 2026-08-02 在 #4535 的授权已写明:「跨概念 → 改名」,且仅保留两条回请条件。叠加 ADR-0112 D9(a) 的同址先例 —— 路线 A(改名 ConnectorRateLimitConfig)是既有裁决的直接适用,不是新决定。方案 B(并集)与 C(收敛+retiredKey)与该裁决相抵触,PM 不予推荐,理由与 agent 的两轴分析一致:B 把同概念异单位的 alias 对焊进契约本体且无任何一层会对矛盾输入报错,正是 AI 批量写错元数据的温床。

    四、真正卡住的是门禁,不是形状

    改名会让 ratchet 认为 6 个 authorable key 消失(实际一个都没离开作者面),而三条补救全部堵死:retiredKey() 需要活着的 def 承载、D2 conversion 的 surface 全是作者路径而 def 改名没有作者路径变化、手编 authorable-surface.json 被 #4650 明令禁止。agent 的两次 sabotage 实测支持这一点(先撞 manifest ratchet「1 previously published schema disappeared」,删该行后撞检查 (a)「6 authorable key(s) disappeared」)。

    所以 A 需要在同一 PR 里给 ratchet 加一张声明式承接表(RENAMED_DEFS,强制旧 def 的每个键都必须在新 def 下存在,否则照红)—— 那是门禁代码,属 #4650 territory,维护者已明确该批门禁洞等自己排期。

    待裁决(唯一一条)

    A:v17 窗口内连门禁承接表一起做 | A′:形状判断不变,按 C6 先例顺延 v18,与 #4650 的窄例外一起落

    这是「是否允许在 v17 收尾期动门禁代码」的排期与所有权问题,不是协议形状问题。已加 needs-user-decision 并解除 assign(无人在做,认领评论保留作审计链)。

    流水线不停

    按本单 C5 时的既定做法(「C5 保持未开工。流水线不停」),C9 无在途 PR ⇒ 串行车道空闲 ⇒ 直接推进 C11。注意基线口径:C9 未落地,故 C11 的目标是 19 → 18,不是 17 → 16。

    另:agent 按第十条军规立了 #4686(两份 RateLimitConfig 全仓零 runtime reader,真正限流的是 packages/runtime/src/security/rate-limit.ts 里第三份互不相识的形状)。已复核该单存在且未认领。它与本单的答案互相影响 —— 若 #4686 结论是「摘掉一侧」,C9 的双源自动消失,两单建议一起看。


    Generated by Claude Code

  8. removed their assignment
    on Aug 2, 2026
  9. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    ✅ 维护者裁决:走 A —— 在 v17 窗口内做,门禁死结用声明式 RENAMED_DEFS 承接表解开

    维护者已在 PM 循环中裁定(2026-08-02)。needs-user-decision 摘除,本单解除阻塞,但排在 C11(PR #4689,已入合并队列)落地之后再派 —— 两簇都改 dual-source-exports.baseline.json,并发必冲突。

    裁决内容

    1. 形状:按 ADR-0112 D9(a) 的既有裁决办 —— 改 connector 侧的名。两侧是两个概念(入站 API 限流 vs 出站 connector 节流),./shared 侧保持 RateLimitConfig 不动。
    2. 门禁:在同一个 PR 里给 ratchet 加声明式 RENAMED_DEFS 承接表,而不是绕过。

    为什么需要承接表(这是本簇真正的工作量)

    dev agent 的分析已确认:ratchet 无法承接 def 改名。改名后旧 def 的 6 个 authorable key 在它眼里等同于「消失」,而三条既有补救全部堵死 ——

    所以 A 的要求是让门禁学会这件事:

    RENAMED_DEFS: { RateLimitConfig → ConnectorRateLimitConfig }(以实施者复核后的实际命名为准)
    规则:旧 def 名下的每一个 key 都必须在新 def 名下存在,否则照红。

    这比今天「基线可手编」的现状更严格,不是放宽 —— 它把「改名」和「悄悄删 key」区分开,而现状是两者都能靠手编蒙混过去。

    追加验收(叠加在原正文的验收之上)

    • RENAMED_DEFS 承接表必须 sabotage 验证:在承接表就位的前提下,故意从新 def 里删掉旧 def 的某一个 key,门禁必须红;贴实际输出。只证「加了表之后门禁绿」是空转证明。
    • 6 个 authorable key 一个都不能少,逐个列出核对表(名字 → 新 def 下的位置)。
    • 零 tombstone、零 ADR-0087 conversion —— 本簇没有 key 被 retire。若你的实现产生了任何一个,说明路线走偏了,停下来升级。
    • changeset:@objectstack/spec major(def 改名对作者是真破坏,与 C11 的 patch 情形不同),写清 FROM → TO 与存量文档的迁移动作。
    • ⛔ 不碰 content/docs/releases/。

    未采纳的路线,及原因(留给后来者)


    Generated by Claude Code

  10. self-assigned this
    on Aug 2, 2026
  11. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    🔒 认领(claim)

    ⚠️ 所有 agent 共用同一个 GitHub 身份,assignee 字段无法区分是谁的认领。若你读到本评论且你的 session ID 不是上面那个,说明本单已被占,请另选一单,不要接手。

    路线已由维护者裁定为 A(v17 窗口内做 + ratchet 加 RENAMED_DEFS 承接表),完整要求见上一条评论。


    Generated by Claude Code

  12. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    PM 复核:ACCEPT —— PR #4695

    会话 session_0176qgxgCXTJCUv4YFLtusP9,PM 循环第 3 轮。复核对着 GitHub 实际 diff 做,不采信 PR 自述;另外自己跑了三项 PR 里没做的独立核查(见下)。

    验收项 复核结果
    PR 形态 ✅ base main(742cebb,含 C11),body 首行 Fixes objectstack-ai/objectstack#4684
    基线 18 → 16 ✅ diff 只删 RateLimitConfig / RateLimitConfigSchema 两行,其余 16 行一字未动
    6 个 authorable key ✅ authorable-surface.json 实际 diff 是 -6/+6 逐条对应,属性名完全一致,无第七行
    零 tombstone / 零 conversion ✅ 13 个改动文件中无 conversions / migrations registry,spec-changes.json 无 diff
    ⛔ content/docs/releases/ ✅ 未触碰
    ⛔ 手编 authorable-surface.json ✅ 未手编 —— 是 gen:schema 经承接表重新生成的
    changeset ✅ @objectstack/spec major,含 FROM → TO、$id 迁移、以及「元数据无需迁移」的明确说明
    范围纪律 ✅ 未动 RateLimitStrategy(非双源,改了对基线零贡献),未留兼容别名

    我另外独立核了三件 PR 没自证的事

    1. 有没有漏掉的 integration 侧 importer(删除已发布导出的典型翻车点)—— 全分支 grep:剩余 RateLimitConfig(Schema) 引用全部是 ./shared 侧(api/endpoint.zod.ts:47、api/registry.zod.ts:379、api/endpoint.test.ts,均解析到 shared/http.zod)、新增 pin 里断言「不存在」的行、以及文档注释。零漏网。
    2. 删掉的 references/integration/http.mdx 有没有悬挂链接 —— 全仓 grep references/integration/http:空。meta.json 条目也同步移除了。
    3. 严格性台账是否真的不需要同步 —— docs/audits/2026-07-unknown-key-strictness-ledger.md grep ratelimit:空,PR 的说法属实。

    RENAMED_DEFS 承接表 —— 通过,且设计比要求的更好

    派发单只要求「旧 def 每个 key 必须在新 def 存在」。实现给了三条不变式,后两条是我没要求但确实必要的:target 必须被本次 build 产出(防拼错 / 防新 def 后来被删),source 必须不再被产出 —— 「两个都在 = 复制而非改名」,而复制正是这张表绝不能洗白的形状。这条堵住了承接表最危险的滥用方式。

    另外承接过来的 key 保留旧 key 的 retired 状态,所以「借改名之机顺手悄悄 retire 一个 key」仍会撞上原有的检查 (b)。净效果确实比现状更严格,而不是开了个口子 —— 这正是路线 A 要的。

    四组 sabotage 全部贴了真实输出,其中两条尤其有说服力:

    两条不阻塞合并的跟进意见

    1. RENAMED_DEFS 用普通对象字面量做查表,key in RENAMED_DEFS 与 renames[def] 会走原型链。当前 def 键形如 <category>/<Name> 必含 /,与 Object.prototype 成员不可能相撞,实际不可达。但对一个「职责就是不可被蒙骗」的门禁,Object.create(null) 或 Object.hasOwn 更稳妥。纯加固,不是缺陷,不必为此返工。
    2. connector.test.ts 里的 KNOWN_STILL_DUAL_SOURCE = ['FieldMapping', 'FieldMappingSchema'] 是故意写成显式清单的(新出现一个就红,而不是写一条从来不成立的「不许同名」)。这意味着 C12 落地时必须同步清空这个数组 —— 已记进 C12 的派发要点,不会漏。

    顺带发现,值得记一笔

    content/docs/references/integration/http.mdx 这个页面本身就是本 bug 的产物:build-docs.ts 的 schemaZodFileMap 按名字全局索引,shared/http.zod.ts 的同名声明覆盖了 connector 的,于是 connector 的 RateLimitConfig 被渲染进了一个叫 integration/http 的页面。改名后它自动归位到 integration/connector.mdx。双源不只骗编译器和作者,也骗了文档生成器 —— 这条在主单里没记录过。

    状态

    CI 6 条里 4 条已绿,CI 与 Lint & Type Check 仍在跑。待全绿后转正并入合并队列;若转红我会按 drive-to-green 处理,不会静默放着。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions