Skip to content

fix(service-automation): 启动路径三条日志的外来 cause 改走结构化 meta (#5661) - #5738

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5661-plugin-log-cause
Aug 6, 2026
Merged

fix(service-automation): 启动路径三条日志的外来 cause 改走结构化 meta (#5661)#5738
os-zhuang merged 2 commits into
mainfrom
claude/issue-5661-plugin-log-cause

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5661

前提重验(基于 origin/main @ 3de5ec8)

三处都在,行号如预期漂了:

issue 里 现状 级别 cause 来源
plugin.ts:454 plugin.ts:452-456 warn 内核服务注册表 / manifest.register()
plugin.ts:592-594 plugin.ts:590-594 error candidate.probe() → 数据源驱动
plugin.ts:931-936 plugin.ts:927-936 error rearmSuspendedWaitTimers 逃出来的值

「刻意不含的两处」也照旧不动:plugin.ts:191 是 throw,plugin.ts:729debug 且文本是我们自己的单行。

一处需要修正 issue 的表述。 issue 说第 3 条的 err 来自 job 服务;逐条读 rearmSuspendedWaitTimers 后不成立 —— 它把自己建模过的每一种失败都自己接住并在自己的 [wait] 记录上报告(store.list() 读取、每次 overdue 的 resume()、每次 job.schedule())。所以 plugin.ts 这个 catch 是「它没建模的东西」的兜底网,message 里那句 "job-service error" 是对最可能肇因的猜测而不是保证。这不改变修法(反而更该结构化交出一个形状无法预测的值),但改变了测试怎么写,也改变了代码注释怎么写 —— 两处都按实际情况记录了,没有照抄 issue 的框架。

改法(与前三单同源,零新词汇)

三处都复用同包 thrown-cause-diagnostics.tsdescribeThrownForLog:message 是不含换行的自足句子,cause 走 logger 的结构化 meta。参数位按 Logger 契约(packages/spec/src/contracts/logger.ts)分:

#4632 要求 error 必须在自己的第一行里同时给出「后果」与「修法」—— 两条都完整保留,只是末尾的 : ${err.message} / Cause: ${err.message} 换成了指向 meta 的一句话:

  • probe:… Check that schema sync ran for this datasource; the driver's own failure is in this record's meta.
  • re-arm:… Fix the job-service error in this record's meta and restart …

没有新增任何 meta 字段名,所以 #5573key/token/secret/password 子串禁令自动满足(describeThrownForLog 早就把 Zod 的 keys 改名成 unrecognized 以躲开 ObjectLogger 的子串脱敏)。

危害:三条不共用同一个下游

  • 第 1 条是 warnstdout,正是 serve 启动静默窗口包住的那条流,而它跑在 init() 里 —— 窗口正开着。BootLogCapture.offer() 只在物理行上找得到级别头时才保留,所以续行是被直接丢弃
  • 第 2、3 条是 errorstderr,不经缓冲;危害是 finding(service-automation): connector 物化失败的 fail() 也是 ${err.message} 单行插值,同 #5048 的类别、另一个接缝 #5575 那一串按行消费者:文件 sink 把续行当成独立记录存,采集端读成无法归属的碎片,grep ERROR 只捞到那条不含事实的头行。这两条恰恰是这个 plugin 里最响的耐久性告警(「suspended runs will NOT survive a restart」「every wait/approval paused before this restart will hang indefinitely」)。

测试

新增 packages/services/service-automation/src/plugin-startup-log-cause.test.ts,13 个用例。照 #5662 的先例全部让真 ObjectLogger 写真字节再读回来 —— spy 只能证明接缝调用了什么,证明不了按行消费者会看到什么,而后者才是 cloud#971 付掉一整条 rc 线的那一半;spy 只在「参数位本身就是被测事实」的三个用例里出现。

三条接缝各自钉住:多行 cause 不进 message、进结构化 meta;参数位正确;无 cause 时输出零字节。第 1 条另有 pretty 格式下「单条 head-bearing 行、事实都在这一行上」的用例。

两处工程取舍写在了代码注释里:

  1. fakeDataEngine 只让 probe 那一次读失败。真实的缺表会让 probe 和 store.list() 都失败,而第二次失败由另一个接缝报告 —— wait-node.ts[wait] … re-arm ABORTED … Cause:,它仍在插值。第一版测试正是被它撞红的(stderr 上出现第二条记录)。把 fixture 收窄到 probe,断言才是在量自己点名的那条记录。同理,plugin-suspended-run-wiring.test.ts 新增了一个按前缀取单条记录的 errorRecord(),因为按 join 后的文本断言分不清匹配来自哪个接缝。
  2. 第 3 条通过替换 rearmSuspendedWaitTimers 这一个调用到达(模块其余部分是真的)。理由见上面的前提重验:用真 store + 真 job 服务构造不出能逃到那个 catch 的失败。与其拿一个不可能发生的 fake 装作可以,不如把这个事实写下来,再让 plugin 自己的 catch、自己的 message 和真 logger 对着一个我们选定的抛出值跑一遍。

fixture 重新裁决而不是重新拼写:plugin-suspended-run-wiring.test.ts 那条 #4420 的 probe 用例原来断言驱动文本出现在 message 里。它现在双向断言 —— message 里没有、meta 里有。单向断言在 cause 被整个丢掉时也会通过。其 capturingLogger 也从只存 String(msg) 改成同时存 meta(两个级别的参数位不同,分别处理)。

实测数字

pnpm --filter @objectstack/service-automation test
  Test Files  63 passed (63)
       Tests  754 passed (754)
pnpm check:durability-log-level
  ✓ 24 durability-critical catch seam(s), all loud, rethrowing or propagating (3 propagating, declared)
pnpm check:startup-registry-verdict
  ✓ 40 startup/open-registry seam(s) across 1446 file(s), 34 read-only (legal), none recording a verdict
pnpm check:init-service-contract        ✓ 34 declared / 1 self-provided / 3 without a workspace provider
pnpm check:type-check-coverage          ✓ 63/78 packages type-checked, ledger unchanged
node scripts/check-nul-bytes.mjs        ✓ 5629 files, no raw ASCII control bytes
npx eslint (三个改动文件)                 ✓ 0

tsc --noEmit(本包无 typecheck script,走 DEBT ledger)在三个改动文件上零错误;既有的 5 个错误全在 engine.test.ts / nested-region-parity.test.ts,与本 PR 无关。

反向验证(方向先预测再跑)

预测的是平实的转红方向:把三处插值还原,message 重新带上换行、meta 里的 issues/error 消失、warn 的实参数从 2 变 1,于是各接缝的钉子转红;而末尾那两个「量差别」的用例是自己内联渲染两种形态、不读 plugin.ts,应当保持绿;三个「无 cause 则零字节」的用例也应当保持绿。

实测完全吻合:

❯ src/plugin-startup-log-cause.test.ts (13 tests | 7 failed)
     × a rejection dump becomes structured issues, never a message spill
     × a plain multi-line rejection keeps its full text, on one line
     × calls warn(message, meta) — `warn` has no Error slot
     × the driver's multi-line failure never reaches the log message
     × calls error(message, undefined, meta) — the contract's third slot
     × the escaping cause goes to meta, the consequence stays in the message
     × calls error(message, undefined, meta) — the contract's third slot
❯ src/plugin-suspended-run-wiring.test.ts (7 tests | 1 failed)
     × reports an unreadable table at boot but still attaches the store
 Tests  8 failed | 746 passed (754)

三条接缝各有至少两条钉子转红(1 → 3 条、2 → 2 条、3 → 2 条),两个测量用例与三个零字节用例如预测保持绿。「level 仍是 error」那条钉子也如预测保持绿 —— 它钉的是级别,不是形状。

范围外发现

builtin/wait-node.ts 还有五处同类插值,其中三处是 #4632 亲自定为 error 的耐久性诊断,而且其中一条是本 PR 写测试时在 stderr 上实测看到的(整段三行驱动错误塞在 msg 里)。已按 objectstack#4949 搜重后独立立单:#5737(finding + domain:services,未打 pm:queue,留给 PM triage 定级)。与 #5660(engine.tsregisterDegradedConnector)不重复 —— 那是另一个文件的另一条记录。本 PR 未碰 wait-node.ts


Generated by Claude Code

…uctured meta (#5661)

`registerRunObject` (warn), the boot `probe()` (error) and the wait-timer re-arm
(error) each interpolated a FOREIGN error's text into the log MESSAGE. They are
the fourth instalment of the family #5048 / #5575 / #5636 closed, and were out
of all three scopes.

`ObjectLogger.write()` emits one timestamp+level record head per call, so a
message carrying newlines becomes several physical lines of which only the first
is attributable. The two `error` seams exist precisely to be read — #4632 raised
them so an operator could find them — and the `warn` one runs in `init()`, inside
`serve`'s boot-quiet window, where `BootLogCapture.offer()` DROPS a head-less
line rather than merely mangling it.

All three now log a newline-free, self-sufficient message and hand the cause to
`describeThrownForLog`, in the slot the `Logger` contract names: `warn`'s second
argument, `error`'s third. #4632's required consequence-and-fix text stays in the
first line; only the trailing interpolation is replaced by a pointer to the meta.

`check:durability-log-level` stays green (24 seams; neither `error` demoted).

Tests: new `plugin-startup-log-cause.test.ts` (13 cases) reads real bytes off a
real `ObjectLogger` per the #5662 precedent, pinning per seam that a multi-line
cause reaches meta and not the message, the argument slot, and that a
cause-less boot writes zero bytes; two closing cases render the old and new
shapes side by side and measure the difference. The #4420 probe case in
`plugin-suspended-run-wiring.test.ts` is re-judged rather than re-spelled: it now
asserts both directions, since a message-only assertion would also pass if the
cause were dropped entirely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 3:13am

Request Review

@github-actions github-actions Bot added the size/l label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-automation.

5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via @objectstack/service-automation)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-automation)
  • content/docs/plugins/packages.mdx (via @objectstack/service-automation)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-automation)
  • content/docs/releases/v9.mdx (via @objectstack/service-automation)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 6, 2026
… check:engine-double-contract passes (#5661)

`check:engine-double-contract` (a lint-job gate, not eslint) pinned the new
`plugin-startup-log-cause.test.ts`: its `fakeDataEngine` declared a bare
`async delete() { return true; }`, and an engine double whose delete does not
route through `assertEngineDeleteDispatch` may accept a call the real
`ObjectQL.delete` refuses — how #4434 shipped a dead REST route with a green
suite.

Removed the method instead of pinning it, and the choice is measured to the
ledger's own standard rather than assumed. An `appendFileSync` marker as the
first statement of that `delete` printed 0 times over this file's suite; the
control — the same injection in `find` — printed 11 times in the same run, so
the silence is evidence, not a broken probe. These seams drive `probe()`,
`list()` and the boot flow pull only; nothing deletes a suspended run.
`SuspendedRunStoreEngine` declares `delete?` optional, so omitting it is the
faithful shape.

Pinning would have certified a method that models nothing, and whose body was
loose in a second way the pin does not address: it answered success while
removing no row. A future case that needs deletion writes a real one and the
gate requires the predicate then; the store also warns when the engine has no
`delete()`, which the byte-exact assertions here would surface at once. The
reasoning is recorded on the fixture so the next reader does not re-derive it.

check:engine-double-contract: OK — 27 pinned, 65 in the DEBT ledger, 1 exempt
(the shrink-only baseline is untouched — no entry added, none lowered).
Package suite: 63 files / 754 tests passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWS4heBoAitLmzCLhcYdbK
@os-zhuang
os-zhuang marked this pull request as ready for review August 6, 2026 03:29
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit dadf542 Aug 6, 2026
24 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5661-plugin-log-cause branch August 6, 2026 03:37
os-zhuang pushed a commit that referenced this pull request Aug 6, 2026
… was measured (#5480)

Base crossing, not a regression on either side.
`packages/services/service-automation/src/plugin-startup-log-cause.test.ts`
arrived on main in #5738 (from #5661) after this branch measured the update
slice and while main still had no update slice at all — so its own CI could not
have flagged its fake engine's `update`, and this ledger could not have recorded
it. It becomes a violation only on the merge ref, where the new slice and the
new file meet for the first time.

Re-measured on the merge with origin/main rather than transcribed: 1 unguarded
engine double at line 170. The package already carries @objectstack/objectql in
devDependencies, so the pin is one line, but the file is under
`packages/services/**` — outside this PR's scope — so it goes to the services
lane's batch alongside the `crud-bulk-intent.test.ts` upgrade step 3 leaves it.
Like every other update entry, it claims no dormancy probe.

Verified on the merged tree: engine-double-contract OK (self-test + run),
delete unchanged at 108 doubles / 91 files / 27 pinned with zero RECONCILED,
update 127 / 111 with 10 pinned and 117 ledgered; query-options-erasure ratchet
holds; check-nul-bytes clean; @objectstack/objectql typecheck clean and 124
files / 2044 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…ck-ai#5480) (objectstack-ai#5754)

* refactor(objectql): extract ObjectQL.update's dispatch into a shared producer-side predicate (objectstack-ai#5480)

`delete` has had one since objectstack-ai#4550 (`resolveEngineDeleteDispatch`), so any test
double standing in for the engine can import the producer's own decision and
cannot be looser than it. `update`'s identical three-way dispatch was an inline
literal throw in `engine.ts` with no exported constant and no reusable function
— so objectstack-ai#5393 could pin its fake's `delete` to the producer and structurally could
not do the same for `update`, on a verb whose predicate branch rewrites every
matching row's fields.

- new `packages/objectql/src/engine-update-dispatch.ts`:
  `resolveEngineUpdateDispatch` / `assertEngineUpdateDispatch` / `scalarUpdateId`
  / `ENGINE_UPDATE_REJECT_MESSAGE` / `ENGINE_UPDATE_DISPATCH_CASES`, all exported
  from `@objectstack/objectql`.
- `ObjectQL.update` now uses it — producer and predicate are one copy.

Behaviour-preserving: the three branches, the `$in` predicate test and the
reject message text are unchanged. Two places where update is not delete are
copied verbatim rather than "improved", and documented as such: `data.id` is
taken without a scalar test and outranks both `where` and `multi`
(objectstack#5748), and the branch reads truthiness, so `where: { id: 0 }` is
not an identifying call.

`engine-update-dispatch.test.ts` drives the REAL engine with a recording driver
over the case set and asserts observed behaviour equals the predicate's verdict
case by case — verified non-vacuous by perturbing the engine's own branch, which
turns the `real ObjectQL.update` half red while the predicate half stays green.

check-engine-double-contract grows an `update` slice on the same criterion:
ledger entries are now keyed on (file, verb) with a required `verb` and a new
DECLARED invariant, the driver-only-member veto applies at every arity (a fake
driver spelling its key `_i` had 19 of its doubles read as engine doubles), and
the self-test covers the update slice on both sides of every decision including
cross-slice credit. Measured: delete unchanged at 108 doubles / 91 files / 27
pinned; update 126 doubles / 110 files, 10 pinned (objectql's own fakes), 116 in
the shrink-only ledger.

Fixes objectstack-ai#5480

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx

* test(scripts): ledger the update double objectstack-ai#5738 landed after this slice was measured (objectstack-ai#5480)

Base crossing, not a regression on either side.
`packages/services/service-automation/src/plugin-startup-log-cause.test.ts`
arrived on main in objectstack-ai#5738 (from objectstack-ai#5661) after this branch measured the update
slice and while main still had no update slice at all — so its own CI could not
have flagged its fake engine's `update`, and this ledger could not have recorded
it. It becomes a violation only on the merge ref, where the new slice and the
new file meet for the first time.

Re-measured on the merge with origin/main rather than transcribed: 1 unguarded
engine double at line 170. The package already carries @objectstack/objectql in
devDependencies, so the pin is one line, but the file is under
`packages/services/**` — outside this PR's scope — so it goes to the services
lane's batch alongside the `crud-bulk-intent.test.ts` upgrade step 3 leaves it.
Like every other update entry, it claims no dormancy probe.

Verified on the merged tree: engine-double-contract OK (self-test + run),
delete unchanged at 108 doubles / 91 files / 27 pinned with zero RECONCILED,
update 127 / 111 with 10 pinned and 117 ledgered; query-options-erasure ratchet
holds; check-nul-bytes clean; @objectstack/objectql typecheck clean and 124
files / 2044 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants