fix(service-automation): 启动路径三条日志的外来 cause 改走结构化 meta (#5661) - #5738
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
… 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
marked this pull request as ready for review
August 6, 2026 03:29
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5661
前提重验(基于
origin/main@ 3de5ec8)三处都在,行号如预期漂了:
plugin.ts:454plugin.ts:452-456warnmanifest.register()plugin.ts:592-594plugin.ts:590-594errorcandidate.probe()→ 数据源驱动plugin.ts:931-936plugin.ts:927-936errorrearmSuspendedWaitTimers逃出来的值「刻意不含的两处」也照旧不动:
plugin.ts:191是 throw,plugin.ts:729是debug且文本是我们自己的单行。一处需要修正 issue 的表述。 issue 说第 3 条的
err来自 job 服务;逐条读rearmSuspendedWaitTimers后不成立 —— 它把自己建模过的每一种失败都自己接住并在自己的[wait]记录上报告(store.list()读取、每次 overdue 的resume()、每次job.schedule())。所以plugin.ts这个 catch 是「它没建模的东西」的兜底网,message 里那句 "job-service error" 是对最可能肇因的猜测而不是保证。这不改变修法(反而更该结构化交出一个形状无法预测的值),但改变了测试怎么写,也改变了代码注释怎么写 —— 两处都按实际情况记录了,没有照抄 issue 的框架。改法(与前三单同源,零新词汇)
三处都复用同包
thrown-cause-diagnostics.ts的describeThrownForLog:message 是不含换行的自足句子,cause 走 logger 的结构化 meta。参数位按Logger契约(packages/spec/src/contracts/logger.ts)分:warn(message, meta?)没有Error位 → cause 在第二参;error(message, error?, meta?)→ cause 在第三参,第二参传undefined(第二参塞原始 error 会让记录额外带堆栈,finding(service-automation): connector 物化失败的fail()也是${err.message}单行插值,同 #5048 的类别、另一个接缝 #5575)。#4632 要求
error必须在自己的第一行里同时给出「后果」与「修法」—— 两条都完整保留,只是末尾的: ${err.message}/Cause: ${err.message}换成了指向 meta 的一句话:… Check that schema sync ran for this datasource; the driver's own failure is in this record's meta.… Fix the job-service error in this record's meta and restart …没有新增任何 meta 字段名,所以 #5573 的
key/token/secret/password子串禁令自动满足(describeThrownForLog早就把 Zod 的keys改名成unrecognized以躲开ObjectLogger的子串脱敏)。危害:三条不共用同一个下游
warn→ stdout,正是serve启动静默窗口包住的那条流,而它跑在init()里 —— 窗口正开着。BootLogCapture.offer()只在物理行上找得到级别头时才保留,所以续行是被直接丢弃。error→ stderr,不经缓冲;危害是 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 行、事实都在这一行上」的用例。
两处工程取舍写在了代码注释里:
fakeDataEngine只让 probe 那一次读失败。真实的缺表会让 probe 和store.list()都失败,而第二次失败由另一个接缝报告 ——wait-node.ts的[wait] … re-arm ABORTED … Cause:,它仍在插值。第一版测试正是被它撞红的(stderr 上出现第二条记录)。把 fixture 收窄到 probe,断言才是在量自己点名的那条记录。同理,plugin-suspended-run-wiring.test.ts新增了一个按前缀取单条记录的errorRecord(),因为按 join 后的文本断言分不清匹配来自哪个接缝。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(两个级别的参数位不同,分别处理)。实测数字
tsc --noEmit(本包无typecheckscript,走 DEBT ledger)在三个改动文件上零错误;既有的 5 个错误全在engine.test.ts/nested-region-parity.test.ts,与本 PR 无关。反向验证(方向先预测再跑)
预测的是平实的转红方向:把三处插值还原,message 重新带上换行、meta 里的
issues/error消失、warn的实参数从 2 变 1,于是各接缝的钉子转红;而末尾那两个「量差别」的用例是自己内联渲染两种形态、不读plugin.ts,应当保持绿;三个「无 cause 则零字节」的用例也应当保持绿。实测完全吻合:
三条接缝各有至少两条钉子转红(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.ts的registerDegradedConnector)不重复 —— 那是另一个文件的另一条记录。本 PR 未碰wait-node.ts。Generated by Claude Code