做 #5575 时核实 `Logger.error` 的类型契约扫到的,与那一单不同包、不同接缝,按 Prime Directive #10 单开。 ### 事实 `packages/spec/src/contracts/logger.ts:37` 声明的契约是: ```ts error(message: string, error?: Error, meta?: Record< string, any >): void; ``` 第二参是 `Error`,`meta` 在**第三**位。`packages/objectql` 的两个模块各自声明了一份**结构化 logger 形状**,把 `error` 写成两参: - `packages/objectql/src/hook-binder.ts:77-81` - `packages/objectql/src/hook-wrappers.ts:26-31` ```ts logger?: { debug: (msg: string, meta?: any) => void; info: (msg: string, meta?: any) => void; warn: (msg: string, meta?: any) => void; error: (msg: string, meta?: any) => void; // ← 第二参是 meta,不是 Error }; ``` 于是三个调用点按这份本地方言把 meta 放在第二位: - `hook-wrappers.ts:341` — `logger.error('[hook] handler failed (onError=log; suppressing)', { … })` - `hook-wrappers.ts:394` — `logger.error('[hook] async handler error (fire-and-forget)', { … })` - `hook-binder.ts:243` — `logger.error('[hook-binder] failed to bind hook', { … })` 注入进去的是 `ctx.logger`(`plugin.ts:290` / `plugin.ts:1670`)。契约类型能结构化地满足这份本地形状(参数少的一方可赋值,`any` 双向兼容),所以 tsc 一句话都不说 —— 方言与契约的冲突只在**运行时**体现。 ### 为什么今天没坏,以及什么时候会坏 只因为宿主注入的实现恰好是 `ObjectLogger`,而它对第二参**按形状分派**(`errorOrMeta instanceof Error`),所以 meta 在第二位也能落进记录。这份宽容本身是契约没有声明的。 契约的另外两个实现 —— `@objectstack/observability` 的 `ConsoleLogger` / `JsonLogger`(`loggers.ts:55` / `113`)—— 忠实按契约来: ```ts error(message: string, error?: Error, meta?: Record< string, unknown >): void { this.emit('error', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) }); } ``` 传进去的对象落在 `error` 位,`error.message` / `error.stack` 都是 `undefined`,`meta` 是 `undefined` —— 于是这三条诊断**整块消失**,只剩一句话。`@objectstack/observability` 正是为了「宿主换一个结构化 logger」而存在,今天仓库里没有任何地方 `new JsonLogger(...)`(除了它们自己的 `child()`),所以属于休眠漂移(observation-class),不是用户今天会撞到的 bug —— 但它会在第一个真正接入生产日志栈的宿主那里生效,而且症状是「日志少了字段」,极难归因。 ### 建议(不预设结论,这里有两个方向) 1. **把三个调用点改成契约形状**(`error(msg, undefined, { … })`),并把两处本地 logger 接口的 `error` 改成三参 —— 或者干脆 `import type { Logger } from '@objectstack/spec/contracts'`,别再自带方言。注意:`ObjectLogger` 在 #5575 之前会**丢弃**契约的第三参,所以这个方向必须建立在 #5575 的 `packages/core/src/logger.ts` 修复之上(已随 #5575 落地)。 2. 或者认为「meta 可以出现在 `error` 位」是我们真心想要的能力 —— 那就应该在**契约里声明**(`error?: Error | Record< string, any >`),而不是让一个实现私下宽容、另两个实现静默丢数据。这一步会给每个 `Logger` 实现加上按形状分派的义务,属于契约级决定,不该由实现方言既成事实地推动。 倾向 1:契约先行,消费端不要方言(Prime Directive #12);2 是把一处宽容升级成所有实现的义务,收益只是省掉一个 `undefined`。 ### 关联 #5575(核实 `Logger.error` 契约时发现;同一 PR 修好了 `ObjectLogger` 丢弃第三参 meta 的缺陷,并统计出 `metadata` / `metadata-protocol` / `client` / `core/security` 约 15 处按契约书写、此前一直静默丢字段的调用点)、#5048 / PR #5572。
Activity
claude commentedon Aug 5, 2026
发现分诊(#4949 纪律):晋级——摘
finding换pm:queue+domain:engine-core,范围裁定为方向 1:三个调用点改契约形状,两处本地 logger 接口改为引@objectstack/spec/contracts的Logger(⛔ 不再自带方言,PD #12)。方向 2(契约里声明error?: Error | Record<string, any>)属契约级决定——若实现中认为必要,走needs-user-decision升级,⛔ 不得默改 spec。fail()也是${err.message}单行插值,同 #5048 的类别、另一个接缝 #5575 已修ObjectLogger丢弃第三参,已落地)。packages/objectql(hook-wrappers.ts/hook-binder.ts)→domain:engine-core(hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775 同锚)。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
认领(PM 会话 session_01V7WetGmnfoXNn8cLieKKmx,
domain:engine-core车道):pm:queue→pm:dispatched,即将派发 os-dev。claude/issue-5637-hook-logger-contract-shape../objectstack-5637packages/objectql/src/hook-wrappers.ts+packages/objectql/src/hook-binder.ts(+ 同包测试 + changeset)。不碰 engine.ts / spec / observability / core。import type { Logger } from '@objectstack/spec/contracts'或其Pick,而不是再手写一份),三个调用点改为error(msg, undefined, { … })。前置 finding(service-automation): connector 物化失败的fail()也是${err.message}单行插值,同 #5048 的类别、另一个接缝 #5575 已落地(ObjectLogger不再丢弃第三参),方向 1 的地基成立。方向 2(契约加宽Error | Record)不采纳:把一处实现的私下宽容升级为所有实现的义务,属契约级决定且收益仅省一个undefined。ObjectLogger按形状分派,第二参undefined落 Error 位无害;ConsoleLogger/JsonLogger从「整块丢 meta」变为如实记录。竞态复查与派发随后。
Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
PM 评审(会话 session_01V7WetGmnfoXNn8cLieKKmx):PR #5780 ACCEPT,待 CI 绿后放行。
已读全量 diff。要点:
HookDiagnosticsLogger = Pick<Logger, 'debug'|'info'|'warn'|'error'>从契约派生、hook-wrappers 单点导出、binder 复用同一类型——两模块形状同源,符合 PD Add comprehensive test suite for Zod schema validation #12 与「最小可用面」。CelFault非 Error、catch 值是 unknown),且明确「catch 值提升进 Error 位是另一个决定,不搭车」——范围纪律好。ObjectLogger的宽容让旧缺陷不可见,复刻宽容什么也测不出;另有真实ObjectLoggerJSON 输出钉住「第二参 undefined 无害」。ConsoleLogger/JsonLogger从整块丢 meta 变为如实记录;BindHooksOptions/WrapDeclarativeOptions收窄经全仓核实无外部调用点。CI 绿后我会转 ready 并挂 auto-merge(当前 GraphQL 配额受限,放行动作在重试队列中,不影响评审结论)。
Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
补充裁决(dev 报告的待定项):
HookDiagnosticsLogger不进 barrel,维持现状——它是 hook 层对契约的内部收窄,公开契约是@objectstack/spec/contracts的Logger本身;外部消费者如需同形状可自行Pick。若将来出现真实的外部具名引用需求,届时一行 barrel 导出另开小单即可,不预支。Generated by Claude Code
fix(objectql): hook layer logs through the Logger contract, not a loc…
chore: bump objectui to 190fbd0
chore: bump the Console objectui pin to 190fbd0 (past the theme-impor…
fix(plugin-security)!: evaluate the insert-side RLS `check` on the ro…