Repository navigation
spec 双源清账 C9:RateLimitConfig / RateLimitConfigSchema(./integration ≠ ./shared)—— 2 条 #4684
Description
Activity
📋 状态:已立单,未开工,未认领 —— 可直接领取
本单由 #4535 的协调会话创建,但没有派发任何 agent,没有写任何代码,没有建任何分支或 worktree。维护者在派发前叫停,改由其他 agent 接手。
已按仓规解除 assign,以免「已认领」的假象让人绕开它(所有 agent 共用一个 GitHub 身份,assignee 字段无法区分是谁的认领 —— 见 CLAUDE.md)。
接手方式
- 按仓规先 assign 自己,并留 claim 评论(含你的 session ID 与分支名
claude/issue-4684-rate-limit-config-dual-source); - 开工前重读本单评论 —— 若出现比你更早、session ID 不同的 claim,说明已被领走;
- worktree-first,然后按上面正文的纪律实施。
接手前必读
- ✅ spec 双源清账主单:基线 52 → 0(2026-08-03 收官)—— #4446 gate 落地后的偿还 worklist #4535 正文(主单):§1–§4 与处置手册第 6/7/8 条。
- spec 双源清账 C8:RetryPolicy / RetryPolicySchema(./automation ≠ ./system)—— 2 条 #4661 / PR feat(spec)!: converge RetryPolicy onto one declaration (#4661, C8) #4670(C8
RetryPolicy):最接近的先例 —— 同样两侧都活、都从元数据根集合可达,用「单一声明放shared/,两个入口 re-export」把代价从 8 个 key 压到 1 个。 - 本簇是 ✅ spec 双源清账主单:基线 52 → 0(2026-08-03 收官)—— #4446 gate 落地后的偿还 worklist #4535 v17 三簇里的第一个,串行派发:C9 落地后再派 C11
HttpRequest,再 C12FieldMapping。不要并行 —— 并发 spec PR 会争抢同一批生成物,2026-08-02 当天已实测三次冲突返工。
Generated by Claude Code
- 按仓规先 assign 自己,并留 claim 评论(含你的 session ID 与分支名
认领: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 在途,都会动同一批生成物:
- feat(spec,objectql,client,plugin-webhooks): honest bulk event contract for predicate writes (#4639) #4677(
api/events.zod.ts,新增 bulk 事件契约) - refactor(spec)!: remove DataEventType 'data.field.changed' — no producer (#4673) #4685(
api/events.zod.ts,移除data.field.changed,已自述与 feat(spec,objectql,client,plugin-webhooks): honest bulk event contract for predicate writes (#4639) #4677 文本冲突) - feat(spec)!: 清空剩余六条 authorWarn 死键 —— book ×2 / job.id / translation.validationMessages / app.homePageId / app.areas[].order (#4667) #4680(六条 authorWarn 死键,size/l,动
authorable-surface.json/spec-changes.json/ ADR-0087 登记表)
它们与本簇源文件不重叠(本簇在
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
- feat(spec,objectql,client,plugin-webhooks): honest bulk event contract for predicate writes (#4639) #4677(
🛑 停下来升级裁决(#4535 处置手册 §6)—— 本簇不是 C8 形状,是 C6 形状
会话:
session_0176qgxgCXTJCUv4YFLtusP9,分支claude/issue-4684-rate-limit-config-dual-source(未推,零提交)。正文的假设「本簇很可能适用 C8 同一手法」经复核不成立。两侧不是「同一概念两种拼法」,是两个不同的概念;而仓规为「两个概念」准备的那条路线(§2 的「改名一侧」)在本簇被门禁堵死,且没有任何可诚实满足的补救。三条可走的路线各自的代价都落在维护者的协议意图上,按 §6 停下来,分析如下。
一、两侧是两个概念,不是两种拼法
shared/http.zod.ts:127integration/connector.zod.ts:300方向 入站 —— 限别人调我们的 API 出站 —— 限我们调外部系统 嵌入点 apis[].rateLimit、ApiEndpointRegistration.rateLimit、httpServer.security.rateLimitconnectors[].rateLimitConfig时间窗 windowMs(毫秒),default 60000windowSeconds(秒),必填,min 1配额 maxRequestsint,default 100maxRequestsmin 1,必填开关 enabledbool,default false无 —— 写了就是开 算法 无(隐含固定窗口) strategy四值枚举,defaulttoken_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/specexports two mutually incompatibleErrorCategoryandRetryStrategytypes(api/errors.zod.tsvsintegration/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」)说的都是这条。- 长期正确性:最高。 入站限流(固定窗口计数)和出站节流(令牌桶 + 读上游 429 头)是两件事,两件事就该有两个名字;将来任一侧要演进都不必顾忌另一侧。零作者面变化,存量文档逐字节不变。
- 让 AI 写元数据不容易写错:最高。 名字本身就是护栏 —— AI 拿到
ConnectorRateLimitConfig不会误以为它能写进apis[];而今天两侧同名,AI 从任一侧的文档抄一段贴到另一侧,得到的是干净解析 + 静默丢键(上面第四行实测)。 - 代价:门禁没有承接 def 改名的能力(见 §二)。要走通只有两条:(i) 给 ratchet 加一张声明式的 def 改名承接表(例如
RENAMED_DEFS: { 'integration/RateLimitConfig': 'integration/ConnectorRateLimitConfig' },并强制旧 def 下每个键都必须在新 def 下存在,否则照红)—— 这不削弱门禁,反而比 authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650 说的「基线可手编」严格;(ii) 不动门禁,则本簇像 C6 一样顺延 v18,等 authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650 的窄例外一起做。这是需要维护者拍板的地方:v17 收尾期动不动门禁代码。
路线 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 conversionconnector.rateLimitConfig.windowSeconds→windowMs(值要 ×1000,是单位换算不是改名)+ D3 链步。 - 但还有两个门禁看不见的形状决定(spec 门禁盲区:可作者化 key 的「默认值 / 约束」变更不被任何 gate、tombstone 或 conversion 记录(#4650 / #4659 同族) #4666 盲区),都必须维护者定:
maxRequests:shared 是default(100)、integration 是必填。合并取默认值 ⇒ connector 漏写从「报错」变成「静默 100」(沉默的放松);合并取必填 ⇒apis[].rateLimit: {}从合法变非法(响亮的破坏,但endpoint.test.ts:28这类现有用法会当场断)。enabled:合并后 connector 凭空获得一个 defaultfalse的开关。今天没人读所以是潜伏的;一旦将来接上执行,每个存量 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 反模式写进契约的路线。
五、我做了什么 / 没做什么
- ✅ worktree
objectstack-issue-4684已建、pnpm install已跑、两次 sabotage 已实测并已全部回滚(git status干净)。 - ✅ 顺带发现并按第十条军规立了 ADR-0049:spec 两份 RateLimitConfig 全仓零 reader,真正在限流的是 runtime 自己的第三份形状 #4686(unassigned):两份
RateLimitConfig全仓零 reader,真正在限流的是packages/runtime/src/security/rate-limit.ts里第三份互不相识的形状。若那单的结论是「摘掉一侧」,本簇的双源会自动消失 —— 建议一起排期。 - ❌ 零提交、零推送、零 PR、零 changeset。 形状未定之前不写投机代码。
- ❌ 未碰 C11 / C12,未碰 spec 门禁盲区:可作者化 key 的「默认值 / 约束」变更不被任何 gate、tombstone 或 conversion 记录(#4650 / #4659 同族) #4666 / authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650 / build-schemas.ts 检查 (b) 用叶名匹配 conversion surface —— 无关簇的
.type就能让一个 tombstone 冒充「已登记迁移」 #4659 /authorable-surface.json在 main 上不是gen:schema的输出 —— 基线手编的字节级实锤(#4650 加固建议) #4663 / packages/spec 的「编译期 pin」是失效的:tsconfig 排除了 *.test.ts,vitest 也不做类型检查 #4642 / spec 生成物没有 merge driver:两个 PR 各改几行,语义上是集合运算,却每次都打成文本冲突 #4675 / 投影类生成物是否该退出仓库 —— 分清「记忆」与「投影」,前者必须留在 PR 粒度,后者可以 CI 重算 #4676,未碰content/docs/releases/,未改 assignee。
裁决后我可以直接接着做 —— A / B / C 三条的落地清单我都已经摸清了。
Generated by Claude Code
- 提示的第 1 步是
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/specexports two mutually incompatibleErrorCategoryandRetryStrategytypes (api/errors.zod.tsvsintegration/connector.zod.ts) — the connector-side pair gets renamed (ConnectorErrorCategory,ConnectorRetryStrategy) so one name means one thing即:完全相同的两个文件、完全相同的「同名不同概念」形状,裁决是改 connector 侧的名。产物就在争议 schema 下方十行(
connector.zod.ts:346ConnectorRetryStrategySchema),注释里写着「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
✅ 维护者裁决:走 A —— 在 v17 窗口内做,门禁死结用声明式
RENAMED_DEFS承接表解开维护者已在 PM 循环中裁定(2026-08-02)。
needs-user-decision摘除,本单解除阻塞,但排在 C11(PR #4689,已入合并队列)落地之后再派 —— 两簇都改dual-source-exports.baseline.json,并发必冲突。裁决内容
- 形状:按 ADR-0112 D9(a) 的既有裁决办 —— 改 connector 侧的名。两侧是两个概念(入站 API 限流 vs 出站 connector 节流),
./shared侧保持RateLimitConfig不动。 - 门禁:在同一个 PR 里给 ratchet 加声明式
RENAMED_DEFS承接表,而不是绕过。
为什么需要承接表(这是本簇真正的工作量)
dev agent 的分析已确认:ratchet 无法承接 def 改名。改名后旧 def 的 6 个 authorable key 在它眼里等同于「消失」,而三条既有补救全部堵死 ——
- 手编
authorable-surface.json→ authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650 明令禁止; - 走 ADR-0087 conversion + tombstone → 语义错误:没有任何 key 真的被 retire,它们只是换了个 def 名下的门牌;伪造 tombstone 会污染 ADR-0087 登记册,正是 build-schemas.ts 检查 (b) 用叶名匹配 conversion surface —— 无关簇的
.type就能让一个 tombstone 冒充「已登记迁移」 #4659 记录过的那类「门禁绿但登记错」。
所以 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/specmajor(def 改名对作者是真破坏,与 C11 的 patch 情形不同),写清 FROM → TO 与存量文档的迁移动作。 - ⛔ 不碰
content/docs/releases/。
未采纳的路线,及原因(留给后来者)
- A′ 顺延 v18:形状判断不变,但把门禁改动挪到 authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650 一起做。维护者选择在 v17 窗口内解决,不再顺延。
- 先裁 ADR-0049:spec 两份 RateLimitConfig 全仓零 reader,真正在限流的是 runtime 自己的第三份形状 #4686(执行面):ADR-0049:spec 两份 RateLimitConfig 全仓零 reader,真正在限流的是 runtime 自己的第三份形状 #4686 实证两份
RateLimitConfig全仓零 runtime reader,真正限流的是packages/runtime里第三份互不相识的形状 —— 若按 ADR-0049 摘掉一侧,本簇的双源与门禁死结会一并消失。但那是更大的协议决定,不塞进 v17 收口。ADR-0049:spec 两份 RateLimitConfig 全仓零 reader,真正在限流的是 runtime 自己的第三份形状 #4686 仍独立有效,不因本簇落地而失效。
Generated by Claude Code
- 形状:按 ADR-0112 D9(a) 的既有裁决办 —— 改 connector 侧的名。两侧是两个概念(入站 API 限流 vs 出站 connector 节流),
🔒 认领(claim)
- 会话 ID:
session_0176qgxgCXTJCUv4YFLtusP9(PM 协调会话,第 3 轮) - 分支:
claude/issue-4684-ratelimitconfig-dual-source - 前置已满足:C11(PR refactor(spec): 双源 C11 收敛 — HttpRequest 类型别名改为 re-export ./shared 的唯一声明 (#4688) #4689)已于
742cebb合并进 main,基线实测 18 条(HttpRequest行已消失,HttpMethod行完好)。串行前置解除,本簇现在开工不会与任何在途 spec PR 争抢生成物。 - 目标:基线 18 → 16。
⚠️ 所有 agent 共用同一个 GitHub 身份,assignee字段无法区分是谁的认领。若你读到本评论且你的 session ID 不是上面那个,说明本单已被占,请另选一单,不要接手。路线已由维护者裁定为 A(v17 窗口内做 + ratchet 加
RENAMED_DEFS承接表),完整要求见上一条评论。
Generated by Claude Code
- 会话 ID:
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/specmajor,含 FROM → TO、$id迁移、以及「元数据无需迁移」的明确说明范围纪律 ✅ 未动 RateLimitStrategy(非双源,改了对基线零贡献),未留兼容别名我另外独立核了三件 PR 没自证的事
- 有没有漏掉的 integration 侧 importer(删除已发布导出的典型翻车点)—— 全分支 grep:剩余
RateLimitConfig(Schema)引用全部是./shared侧(api/endpoint.zod.ts:47、api/registry.zod.ts:379、api/endpoint.test.ts,均解析到shared/http.zod)、新增 pin 里断言「不存在」的行、以及文档注释。零漏网。 - 删掉的
references/integration/http.mdx有没有悬挂链接 —— 全仓 grepreferences/integration/http:空。meta.json条目也同步移除了。 - 严格性台账是否真的不需要同步 ——
docs/audits/2026-07-unknown-key-strictness-ledger.mdgrepratelimit:空,PR 的说法属实。
RENAMED_DEFS承接表 —— 通过,且设计比要求的更好派发单只要求「旧 def 每个 key 必须在新 def 存在」。实现给了三条不变式,后两条是我没要求但确实必要的:target 必须被本次 build 产出(防拼错 / 防新 def 后来被删),source 必须不再被产出 —— 「两个都在 = 复制而非改名」,而复制正是这张表绝不能洗白的形状。这条堵住了承接表最危险的滥用方式。
另外承接过来的 key 保留旧 key 的 retired 状态,所以「借改名之机顺手悄悄 retire 一个 key」仍会撞上原有的检查 (b)。净效果确实比现状更严格,而不是开了个口子 —— 这正是路线 A 要的。
四组 sabotage 全部贴了真实输出,其中两条尤其有说服力:
- 4.4 只加回一个纯类型别名 → 编译器 API 那条断言红,其余 41 条运行时断言全绿。这是 packages/spec 的「编译期 pin」是失效的:tsconfig 排除了 *.test.ts,vitest 也不做类型检查 #4642「编译期 pin 空转」之后,本仓类型级 pin 有效性的直接实证。
- 4.5 通用不变式抓到一个全新的双源名(
CorsConfig),而不是只钉死已知那两个。
两条不阻塞合并的跟进意见
RENAMED_DEFS用普通对象字面量做查表,key in RENAMED_DEFS与renames[def]会走原型链。当前 def 键形如<category>/<Name>必含/,与Object.prototype成员不可能相撞,实际不可达。但对一个「职责就是不可被蒙骗」的门禁,Object.create(null)或Object.hasOwn更稳妥。纯加固,不是缺陷,不必为此返工。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
- 有没有漏掉的 integration 侧 importer(删除已发布导出的典型翻车点)—— 全分支 grep:剩余
- added 4 commits that reference this issue
on Aug 3, 2026
#4535 的 C9 簇,v17 收口三簇之一。基线行:
基线现为 19 条,本簇目标 19 → 17。
本簇在作者面上 —— tombstone 风险是真的
静态引用图确认
RateLimitConfigSchema从BUILTIN_METADATA_TYPE_SCHEMAS可达。已知嵌入点(开工请自行复核):./sharedshared/http.zod.ts:144声明;api/endpoint.zod.ts:47与api/registry.zod.ts:379的rateLimit:./integrationintegration/connector.zod.ts:336声明任何可作者化 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,开工前必读)
retryDelayMsvsbackoffMs),并集保全不可行 —— 同时声明两者是 alias 反模式,必须选一个并对被丢的键走完整 ADR-0087。packages/spec/authorable-surface.json(authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650)。gen:schema只允许因新增 key 而重写它。.type就能让一个 tombstone 冒充「已登记迁移」 #4659):检查 (b) 按 leaf name 匹配 conversion surface,无关 conversion 也能满足。自己枚举ALL_CONVERSIONS的 clause,确认以你 retire 的那个叶名结尾的全仓只有 1 条且就是你的(C8 的做法)。.default()或.min()/.max()改变,必须用手写运行时测试钉住,并在 changeset 里显式写明;若存量文档会因此改变行为,用 conversion 显式物化旧值(C8 的做法:把 pre-17 默认值写进每个省略了它的存量文档,已部署栈行为不变)。git fetch origin main判一次;要合并时:字符串数组可集合合并,spec-changes.json是对象数组,必须跑gen:spec-changes(集合合并会丢条目,已把 CI 打红过)。pnpm install --filter @objectstack/spec...走缓存约 2.5 秒,生成器秒级。解完冲突务必本地跑对应check:*再推。content/docs/releases/。验收
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。docs/audits/2026-07-unknown-key-strictness-ledger.md。@objectstack/specmajor,含 FROM → TO 与迁移指引。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