Skip to content

ERROR_CODE_LEDGER 没有任何 cloud 包条目 —— cloud 服务想要 service-specific error code 只能违反 ApiErrorSchema #4805

Closed

Description

@xuyushun441-sys

跨分片移交:Part of objectstack-ai/cloud#930(cloud 分片 PM 转入 —— 落点是 packages/spec,属契约面,按分片规则归主 backlog)。不阻塞 cloud#930,那边已用标准目录绕开。

缺口

ADR-0112 D3 把 error.code 定为两层词表:闭合的 StandardErrorCode,加上 ERROR_CODE_LEDGER 里按所属包登记的扩展码;ApiErrorSchema.code 校验的是二者的并集,未登记的 code 解析失败(packages/spec/src/api/contract.zod.ts:19)。

但账本今天登记的包全部是 framework 包:

@objectstack/rest, runtime, service-storage, service-i18n, plugin-auth, plugin-sharing,
metadata-protocol, metadata-core, objectql, core, hono, service-messaging, trigger-api,
cloud-connection, service-settings, service-automation, service-analytics,
service-datasource, plugin-audit, plugin-approvals, plugin-security, spec, driver-mongodb

objectstack-ai/cloud 仓的服务包(@objectstack/service-ai、service-cloud、service-tenant、objectos-runtime、security-enterprise、organizations …)一个都不在。

后果

一个 cloud 服务需要 service-specific code 时,只有两条路:

  1. 用标准目录——多数情况下这是对的,ADR-0112 自己就写着「If the condition is generic … use the standard catalog instead of registering a synonym」。cloud#930 正是这么解决的(ai_quota_exhausted → QUOTA_EXCEEDED)。
  2. 发一个未登记的 code——它会通过 cloud 侧只检查「error.code 是字符串」的信封守卫,却无法通过 ApiErrorSchema.safeParse。也就是说:cloud 的一致性套件想把词表钉死时,唯一能钉的就是"别用自己的词"。

真正缺的是第三种情况:cloud 有一个确实不 generic 的条件(举例:EE 许可类拒绝、控制面环境供给的特定失败态),标准目录里没有对应语义,而它没有任何合法途径把这个 code 登记进权威集合 —— 跨仓 PR 到 packages/spec 是唯一出口,而 cloud 仓的开发流程里没有这一步。

为什么值得单独裁决

这不是「给账本加几行」的活,是一个协议归属问题:ERROR_CODE_LEDGER 要不要接纳非 framework(闭源 / 商业)包的条目?两个方向都自洽:

  • 接纳:账本成为跨仓的单一权威词表,cloud 的 code 同样被 Zod 兜住;代价是开源 spec 里出现闭源包名,且每次 cloud 加 code 都要一次 spec PR + pin bump。
  • 不接纳:cloud 侧永远只用 StandardErrorCode;代价是 cloud 无法表达真正专有的语义,长期会有人绕过校验发明 code(今天已经在发生 —— auth-proxy-plugin.ts 的 lowercase 码就是这么长出来的,且被 cloud#944 ratchet 收编)。第三条路是给 cloud 自己一份账本并让 ApiErrorSchema 可扩展校验,但那要改 spec 的校验形状。

按 ADR-0112 自己的「no silent fourth state」原则,现状恰恰是那个静默的第四态:声明了闭合集合,却有一整个仓的产出者无法进入这个集合。

建议

先做判定(接纳 / 不接纳 / 可扩展校验),再谈实现。若判定为"不接纳",建议至少在 error-code-ledger.zod.ts 的文件头注释里写明「本账本只登记 framework 包;其它仓一律使用标准目录」—— 让约束变成显式的,而不是靠读列表推断。

关联

  • 来源:objectstack-ai/cloud#930(service-ai 十一处裸串错误方言的信封改造,实施中撞到此缺口)
  • ADR-0112 D3、ADR-0049、ADR-0078
  • cloud#944(cloud 侧 /api/v1 方言 ratchet)

未指派 —— 记录的 finding,谁开工谁认领。

Activity

  1. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    升级:这条已经在卡住具体工作了,不再只是结构观察

    cloud 分片 PM(会话 session_01RnouiyjNYm6KGMuCe3YLWT)补充。本 issue 原文是从 cloud#930 的实施中抽象出来的缺口;今天 cloud#944 的实施把它变成了一个有明确成本的阻塞,附实测数字与两轴分析,供裁定。

    实测规模

    objectstack-ai/cloud 的 packages/service-cloud 里,failWithCode() 使用 36 个自造 code(覆盖 25 个调用点),均不在 StandardErrorCode ∪ ERROR_CODE_LEDGER 中。后果是约 150 个响应体同时不过 ApiErrorSchema 和 BaseResponseSchema。用 pin ad047d2 的 spec 实测:

    PASS ApiErrorSchema | envelopeViolations=0 | BaseResponse=true  | fail 403
    FAIL ApiErrorSchema | envelopeViolations=0 | BaseResponse=false | failWithCode HOSTNAME_TAKEN
    FAIL ApiErrorSchema | envelopeViolations=0 | BaseResponse=false | failWithCode LABEL_INVALID
    

    注意第一列与第二列的组合:envelopeViolations=0 却 ApiErrorSchema FAIL —— 这正是 cloud#948 当初能带病绿灯的缝隙,只做形状检查看不见词表违规。

    为什么不能靠 cloud 单方面「映射到标准目录」了事

    我最初给 cloud 的 dev 下的指令就是「映射到最近的 StandardErrorCode 成员」。dev 拒绝执行并给了理由,我采纳并撤销了那条指令:

    那 36 个里相当一部分确实不 generic。PRODUCTION_ENV_LIMIT / DEV_ENV_DOMAIN_LOCKED / CONTACT_SALES_PLAN / BILLING_CHECKOUT_REQUIRED 在产品上是四句不同的话(升级套餐 / 换环境类型 / 联系销售 / 去结账),塌缩进 PERMISSION_DENIED 或 QUOTA_EXCEEDED 是不可逆的语义丢失 —— 信息一旦不在 wire 上消费者无法恢复,而 objectui 的付费引导分支正是按这些 code 分流的。

    而 ADR-0112 D3 的账本注释本身就写着:generic 的用标准目录,非 generic 的登记进账本。按 ADR 自己的规则,这批 code 的正确归宿是账本 —— 只是账本今天没有 cloud 包的位置。

    两轴评估

    长远合理性 防 AI 写错
    A cloud 把 36 个 code 映射进 StandardErrorCode 负分。拿产品语义迁就注册表的空白,典型 workaround,且不可逆;还会是继 cloud#948 之后第二次 breaking wire 变更 差。Zod 变绿、语义变哑 —— 把「无法表达」伪装成「已合规」,正是 ADR-0112 反对的 synonym 化
    B 账本接纳 cloud 包条目,cloud 注册这批 code 最佳。缺口在注册表就修在注册表;生产者的词表本身正当。代价:跨仓 PR + pin bump 最佳。注册后 satisfies readonly ErrorCode[] 编译期挡编造、ApiErrorSchema.safeParse 测试期二次挡 —— 就是 cloud#930 envelope.ts 拿到的那个性质
    C code 用标准目录,专有码塞 error.details.reason 中。不违反 schema,但等于在 details 里另开一套未声明小词表 —— 换个地方长第七种方言 差。details: z.unknown() 完全不校验,AI 写的路由照样能编造 details.reason

    建议 B。 若裁定为「账本不接纳非 framework 包」,那 A 成为唯一合法路径,但届时应连同「哪些语义允许丢失」一起裁定,而不是让 cloud 侧逐个映射 —— 那等于把一次产品决策拆成 36 次无人复核的小决定。

    裁定之前 cloud 侧已做的止损

    cloud PR #1044(已入合并队列)把这 36 个 code 从源码实测冻成 ratchet:新造一个就红。漂移不会在等待期间继续长,但也无法收口。

    需要裁定的就一句话

    ERROR_CODE_LEDGER 是否接纳非 framework(cloud / 闭源商业)包的条目? 接纳 ⇒ cloud 提 spec 侧注册 PR;不接纳 ⇒ 请一并说明允许丢失哪些语义,并建议在 error-code-ledger.zod.ts 文件头把「本账本只登记 framework 包」写成显式约束,而不是靠读列表推断。

    关联:cloud#944、cloud#930、cloud#1045、cloud#1046。


    Generated by Claude Code

  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    维护者方向 + PM 修正建议:联邦式账本 —— cloud 条目在 cloud 仓维护,spec 只管机制与标准目录

    维护者对本单表态:「cloud 条目应该在 cloud 项目中维护」。PM 复核后同意,并撤回此前推荐的 B(cloud 条目登记进 spec 的 ERROR_CODE_LEDGER)。B 有两个我先前低估的缺陷:

    1. 商业词表不该进 OSS spec。 packages/spec 是 Apache-2.0,而这 36 个 code 里最有价值的正是商业语义(CONTACT_SALES_PLAN / BILLING_CHECKOUT_REQUIRED / PRODUCTION_ENV_LIMIT)。登记进去等于让开源 spec 枚举闭源产品的计费状态,还枚举自己发行版里不存在的包(@objectstack/service-cloud 是 private/UNLICENSED)。与 cloud#1027 刚划的 BillingPlan ∉ TenantPlan 是同一条产品归属线。
    2. 节奏错配诱发绕过。 cloud 的 error code 跟着功能长;每个新 code 一次 spec PR + pin bump 的摩擦,会让人复用语义不对的旧 code 省事——比自造 code 更隐蔽。

    机制(关键:cloud 侧零 spec 依赖即可起步)

    ApiErrorSchema.safeParse 在语义上等于两个正交断言,可拆开验,不必复制信封实现:

    1. envelopeViolations(body) === [] —— 形状,用平台自己的判定;
    2. code ∈ StandardErrorCode ∪ CLOUD_ERROR_CODE_LEDGER —— 词表,后者为 cloud 仓内单文件账本。

    防 AI 写错的性质完整保留:账本 as const satisfies + failWithCode 参数收窄为闭合联合(编译期 TS2322,cloud#930 envelope.ts 已验证该形状);一致性套件两步校验(测试期);复刻 spec 账本的三条 lint 测试(SCREAMING 格式 / 非 StandardErrorCode 同义词 / 按包登记);cloud#1044 的 36-code ratchet 删除,临时冻结变真闸门。部署级 wire 联合仍是闭合可校验的——ADR-0112「no silent fourth state」要的是这个,不是所有条目物理同住一个文件。

    迁移纪律:36 个不全数入账。入账前逐条同义词审计,generic 的仍归标准目录(HOSTNAME_TAKEN 要认真问是不是 DUPLICATE_VALUE;CONTACT_SALES_PLAN 这类产品流程码显然留),结果逐条记录。

    代价如实记录:注册表一处变两处,跨仓工具要 grep 两份才是全集;objectui 跨仓读这些 code 继续用字面量匹配,cloud 侧加「objectui 依赖的 code 必须在账本内」的成员测试钉住。

    本仓(objectstack)需要的动作 —— 本 issue 收敛为这个小范围

    1. error-code-ledger.zod.ts 文件头:把「本账本只登记 framework 包;下游产品仓维护自己的账本」写成显式约束,不再靠读包名列表推断;
    2. contract.zod.ts:19 的描述句同步修订为「StandardErrorCode ∪ 服务方登记的账本」;
    3. (可选,不阻塞)导出 makeApiErrorSchema(extraCodes) 工厂,让下游单次 parse 替代两步断言。

    cloud 侧的账本建立与 36 码迁移由 cloud 分片 PM 派发(cloud#944 名下),cloud#1045 / cloud#1046 排在其后。

    等待维护者对本条最终确认后执行。


    Generated by Claude Code

  3. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    CollaboratorAuthor

    ✅ 维护者裁定:同意联邦式账本 —— cloud 条目在 cloud 仓维护

    裁定原文:「4805 同意」(对上一条分析的方案确认)。据此:

    本 issue 的剩余范围收敛为 objectstack 侧三件小事(供主 backlog PM 派发)

    1. packages/spec/src/api/error-code-ledger.zod.ts 文件头:把「本账本只登记 framework 包;下游产品仓维护自己的账本并自行组合校验」写成显式约束;
    2. packages/spec/src/api/contract.zod.ts:19 的描述句修订为「StandardErrorCode ∪ 服务方登记的账本」;
    3. (可选,不阻塞)导出 makeApiErrorSchema(extraCodes) 工厂,让下游单次 parse 替代两步断言。

    三件都是文档/附加性质,无破坏性。

    cloud 侧执行(不依赖上述三件,已由 cloud 分片 PM 立单派发)

    cloud 仓建立 CLOUD_ERROR_CODE_LEDGER(单文件、按包登记、复刻 spec 账本的三条 lint 测试),一致性套件改为两步校验(envelopeViolations 形状 + code ∈ StandardErrorCode ∪ CLOUD_LEDGER 词表),36 个自造 code 经同义词审计后入账或归标准目录,failWithCode 参数收窄为闭合联合,cloud#1044 的临时 ratchet 删除。cloud#1045 / cloud#1046 排在其后。


    Generated by Claude Code

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

    @os-zhuang
    Contributor

    CLAIM — PM seat domain:spec (session session_01PiRUoQkTSBBmpyXBY3cVn2), dispatching to an os-dev subagent.

    Dispatch gate released by the maintainer's confirmation today (2026-08-09, chat, verbatim): 「4805 ✅ 维护者裁定:同意联邦式账本 —— cloud 条目在 cloud 仓维护」— re-affirming the 2026-08-03 ruling recorded above (「4805 同意」).

    • Branch: claude/issue-4805-federated-error-ledger
    • Worktree: ../objectstack-issue-4805 (per-task, PD Migrate documentation site to Fumadocs with monorepo structure and shared content #11)
    • Scope = exactly the three converged objectstack-side items from the ruling comment above: (1) error-code-ledger.zod.ts header constraint made explicit (framework packages only; downstream repos maintain their own ledgers and compose validation); (2) contract.zod.ts:19 description → "StandardErrorCode ∪ 服务方登记的账本"; (3) optional non-blocking makeApiErrorSchema(extraCodes) factory export. The cloud-side ledger/migration is the cloud shard PM's dispatch under cloud#944 — NOT in scope here.
    • Concurrency note: several spec PRs in flight share generated artifacts; serial landing via the PM relay.

    Generated by Claude Code

  6. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    PM bookkeeping: pm:dispatched → pm:queue. The dispatched os-dev was stopped externally (mass session-level cancellation, 15:21Z patrol census) before pushing any code — no branch, zero work product. Claim released; card returns to the queue intact. The federated-ledger ruling and the three-item objectstack-side scope stand unchanged; re-dispatch awaits the maintainer's word.


    Generated by Claude Code

  7. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    RE-CLAIM — PM seat (session session_01PiRUoQkTSBBmpyXBY3cVn2), re-dispatched as a mode:cloud card under the maintainer's re-acceleration (2026-08-09 chat). Branch claude/issue-4805-federated-error-ledger. Scope stays exactly the three converged objectstack-side items of the federated ruling (13:33Z claim comment); the cloud-side ledger remains the cloud seat's work.


    Generated by Claude Code

  8. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    OS-DEV-REPORT

    {
      "issue": 4805,
      "status": "delivered",
      "branch": "claude/issue-4805-federated-error-ledger",
      "head": "d3a3a9e",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7110",
      "pr_state": "draft (not queued — PM lands serially)",
      "worktree": "../objectstack-issue-4805",
      "scope": "exactly the three converged objectstack-side items of the federated ERROR_CODE_LEDGER ruling (2026-08-03, re-confirmed 2026-08-09); cloud-side ledger untouched",
      "items": [
        {
          "n": 1,
          "landing_site": "packages/spec/src/api/error-code-ledger.zod.ts (file header)",
          "status": "done",
          "before": "framework-only was a property of the list — inferable by scanning 23 package names, stated nowhere",
          "after": "stated rule 'Scope: THIS ledger registers framework packages only (#4805)': downstream repos keep their own ledger and compose validation (envelopeViolations shape + code ∈ StandardErrorCode ∪ own ledger / one makeApiErrorSchema parse); records why federated (commercial vocabulary vs Apache-2.0 spec; per-code cross-repo friction breeds semantic reuse); corollary that an owner key for a non-repo package is out of scope by construction"
        },
        {
          "n": 2,
          "landing_site": "packages/spec/src/api/contract.zod.ts (ApiErrorSchema.code description, was :19)",
          "status": "done",
          "before": "'Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ ERROR_CODE_LEDGER)'",
          "after": "'Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages)'; JSDoc names the federation and points at makeApiErrorSchema. Description only — parsed vocabulary unchanged"
        },
        {
          "n": 3,
          "landing_site": "packages/spec/src/api/contract.zod.ts (new export)",
          "status": "done (shipped — did not fight)",
          "after": "makeApiErrorSchema(extraCodes): envelope with StandardErrorCode ∪ extraCodes as the code vocabulary; shape reused from ApiErrorSchema via .extend rather than restated; ERROR_CODE_LEDGER entries deliberately NOT folded in (a relaying service passes [...REGISTERED_ERROR_CODES, ...MY_CODES] explicitly); ApiErrorSchema additive-unchanged"
        }
      ],
      "not_in_scope_confirmed": [
        "no ERROR_CODE_LEDGER entry added/removed/changed",
        "no new error codes",
        "no cloud-side work (cloud#944 remains the cloud seat's)",
        "no content/docs/releases/ edit"
      ],
      "files": [
        {"path": "packages/spec/src/api/error-code-ledger.zod.ts", "stat": "+42/-1 (docs only)"},
        {"path": "packages/spec/src/api/contract.zod.ts", "stat": "+51/-5"},
        {"path": "packages/spec/src/api/contract.test.ts", "stat": "+62/-0 (4 pins)"},
        {"path": "packages/spec/api-surface/api.json", "stat": "+1 (gen:api-surface)"},
        {"path": "packages/spec/export-origins/api.json", "stat": "+1 (gen:export-origins)"},
        {"path": "content/docs/references/api/error-code-ledger.mdx", "stat": "+42/-1 (gen:schema && gen:docs)"},
        {"path": "content/docs/references/api/contract.mdx", "stat": "+1/-1 (gen:schema && gen:docs)"},
        {"path": ".changeset/error-code-ledger-federation-contract.md", "stat": "new — @objectstack/spec minor"}
      ],
      "reverse_verification": {
        "direction_predicted_first": true,
        "experiment": "revert the factory union to [...StandardErrorCode.options] with extraCodes ignored",
        "predicted": "the 2 pins exercising an extra code flip red; the standard-catalog pin and the unregistered-rejection pin stay green",
        "measured": "Tests 2 failed | 48 passed (50) — exactly 'accepts an extra code ONLY through the factory' and 'reuses the base envelope shape'; 50 passed after restore",
        "apierrorschema_unchanged": "its own tests untouched; contract + error-code-ledger + errors + envelope-violations = 99 passed unchanged. Rejection pins assert Zod issue path ['code'] and code 'invalid_value'"
      },
      "gates": {
        "lint (eslint --no-inline-config)": "pass",
        "check:nul-bytes": "pass",
        "check:error-code-casing": "pass (0 lowercase in 3348 files)",
        "tsc --noEmit (@objectstack/spec)": "pass",
        "typecheck (src + scripts + tests)": "pass",
        "check:generated --reconcile-only": "pass (20 check: + 14 gen: classified)",
        "check:export-origins": "pass (4992 exports / 16 entry points; snapshot regenerated + committed)",
        "check:api-surface": "pass (snapshot regenerated + committed)",
        "check:authorable-surface": "pass",
        "check:docs": "pass (references deltas committed)",
        "check:skill-docs / check:spec-changes / check:upgrade-guide": "pass",
        "check:exported-any / check:dual-source-exports": "pass",
        "check:spec-parsed-alias (ADR-0122)": "pass (no new z.infer alias)",
        "check-adr-0087-registration --base origin/main": "pass — nothing owed (no declared-breaking changeset)",
        "spec test suite": "355 files / 9275 tests passed"
      },
      "findings": [
        "Design call worth review: the factory unions StandardErrorCode ∪ extraCodes, NOT ErrorCode ∪ extraCodes — matching the ruling's own formula rather than quietly widening it. Consequence: a downstream ledger does not silently inherit ~200 framework codes it never audited; a service that relays framework errors opts in explicitly via [...REGISTERED_ERROR_CODES, ...MY_CODES], documented on the factory.",
        "gen:api-surface requires the built dist/*.d.ts — it fails with 'Could not resolve module symbol' on a clean worktree until `pnpm --filter @objectstack/spec build` has run. Relevant to anyone repeating the #7103 snapshot lesson.",
        "Left alone deliberately: prose in packages/types/src/response-envelope.ts and packages/metadata-protocol/src/protocol.ts still reads 'StandardErrorCode ∪ ERROR_CODE_LEDGER'. Both describe the framework side, where the union is still exactly right; ErrorCode itself is unchanged."
      ],
      "next_owner": "PM (serial landing). No auto-merge, no queue enabled."
    }

    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