Skip to content

fix(plugin-email,plugin-security): durability SUMMARY reports must print against an error-less sink - #10396

Open
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-9748-summary-durability-reports
Open

fix(plugin-email,plugin-security): durability SUMMARY reports must print against an error-less sink#10396
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-9748-summary-durability-reports

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #9748

Two durability summary reports were spelled logger?.error?.(…) — an optional call that emits nothing when the sink has no error method, and error is declared optional on both sinks. #9657 repaired the six per-row reports of this shape; it could not reach these two, because check:durability-log-level only judges a call inside a catch and a summary sits after the loop.

The premise was re-verified against origin/main before any code was written (801296050): both sites still carried the unguarded spelling, and the gate was still blind to them.

The harm, reproduced before it was fixed

Both new tests were written first and run against unmodified source. Both failed, identically — the summary emitted nothing at all:

× [#9748] the batch SUMMARY also reaches a sink with NO `error` — at warn, not in silence
  AssertionError: expected [] to have a length of 1 but got +0
      Tests  1 failed | 17 passed (18)      ← plugin-email
      Tests  1 failed | 51 passed (52)      ← plugin-security

⭐ The sharp part: both summaries count failures whose per-row line #9657 did repair. So against an error-less sink the repair made the split worse, not better — the detail arrived at warn while the total vanished, and the counts and the detail reported through different channels. In permission-set-projection.ts it is worse still: the else branch carrying the info "reconciled" line is skipped too, so such a sink heard neither.

After the fix, both files are green (18/18 and 52/52; full suites 420/420 and 1305/1305).

Which option, and why

The card recorded two directions and deliberately did not rule between them. I took both — repair the two sites and extend the gate — because the population turned out to be real and bounded. The measurement came first:

population under the candidate criterion count
judged — branch reaches error/fatal 2 (exactly the two sites this card names)
discovered, deliberately not judged — branch reports at info/warn 1 (objectql/plugin.ts "Schema sync complete")
dropped before the level filter — boolean latch, not a counter 3

The naive version of the criterion — "any branch guarded by a counter a durability catch mutated, that logs, must be loud" — reddens 4 correct sites (database-loader.ts ×2, protocol.ts, objectql/plugin.ts). Every one of them is a recovery or success line that is correctly info. Baselining those would have parked correct code in a shrink-only ledger whose header calls every line debt — the exact anti-pattern #5241 removed. Two narrowings, both measured, both load-bearing:

  1. Accumulator, not any mutation. flag = true in a catch is a say-it-once latch, not a failure count. Keying on ++/+= drops all three latch sites.
  2. The level is never second-guessed. The limb asks only check-durability-degradation-log-level: loggerLevel cannot see the (logger.error ?? logger.warn)(…) fallback, so a loud catch reads as silent-swallow — and the spelling it CAN see prints nothing #9657's question — "you reached for error; against a sink with no error, does anything print?" — so it is a narrowing of the question applied to a wider population, not a new semantic judgement. A branch that reports at info/warn is discovered, counted, and not judged.

The only way to trip the limb is logger.error?.(…) with no fallback, which is never correct. #9657's rule is unchanged and not weakened, and no baseline was wideneddurability-degradation.baseline.json is still entries: [].

The reject side, asserted positively

A matcher that quietly stopped matching also prints zero findings. So the gate now prints its census on every run, and the self-test pins the skip count rather than merely observing no findings:

SUMMARY LIMB [durability-degradation-log-level] (#9748): 2 counter-guarded summary report(s)
OUTSIDE any catch reach `error`/`fatal` and are judged on SPELLING alone (0 reach it only
through an optional call); 1 further branch(es) report at a level this limb never
second-guesses (info/warn) and are counted here instead of judged.
  summary  packages/objectql/src/plugin.ts:1558  counter `synced` from syncSchemasBatch()  → not judged (author-chosen level)  [info@1559]

Eight new self-test cases pin both directions plus the reject side (expectSummaryJudged / expectSummarySkipped), including the two controls that a broken matcher would pass: a boolean latch must yield 0 judged, and an unrelated condition next to the same catch must yield 0 — while the info branch must yield exactly 1 skipped. Self-test: 63 case(s) passed.

Ablation — the limb is shown going red

The gate is a pure source-text AST scanner (dist is in its SKIP_DIRS), so no build sits in this path and no rebuild leg applies. Mutation and restore were each confirmed on disk by grepping both the injected and the removed text, never by an editor's exit code.

  • Mutation (both repairs reverted to ?.error?.(), confirmed on disk: injected 1+1, removed 0+0) → gate exit 1, ✗ 2 durability report(s) degrade quietly … 0 inside a catch, 2 in a SUMMARY the catch feeds (#9748). Direction as predicted: red. The skip count held at 1 through both legs, so the matcher did not collapse.
  • Restoregit diff --exit-code = 0 (byte-identical), gate exit 0, census back to 0 conditional.

Verification

All gates below were run on the final commit 31f2ebced (git rev-parse --short HEAD), derived with node scripts/pm/dispatch-gates.mjs with no path arguments — 6 paths, 12 path-matched families plus 6 convention-triggered ones. Exit codes were captured before any pipe. Full detail in the report comment on #9748.

Clause ② declared: not reachable — the path limb is packages/spec/src/** and this change touches none of it.

Changeset — judged by publish surface, not by habit: @objectstack/plugin-email and @objectstack/plugin-security are both published (private=false), and this changes their runtime logging behaviour on a path a host can observe. A changeset is owed; this is not skip-changeset, and no such label was applied.

⛔ Left in draft, not merged, auto-merge not enabled — landing is the maintainer's.


Generated by Claude Code

… must print against an `error`-less sink

`SweepLogger.error` and `ProjectionLogger.error` are declared OPTIONAL, and
both batch summaries were spelled `logger?.error?.(…)` — an optional call that
emits NOTHING when the method is absent. #9657 repaired the six per-row reports
of this shape; it could not reach these two, because the gate only judges a
call inside a `catch` and a summary sits after the loop.

That made the split WORSE, not better: against a `{ info, warn }` sink the
per-row detail now landed at `warn` while the count of failures stayed silent,
so the detail and the total reported through different channels. Both summaries
now reach for `error` and fall back to `warn`, never to silence.

Also extends `check:durability-log-level` with a SUMMARY limb so the class
cannot regress: a report keyed on the counter a durability-critical catch
accumulated into is judged on SPELLING alone — the limb never second-guesses a
chosen level. Measured before it was proposed: 2 judged (both sites here), 1
discovered and deliberately not judged (`objectql/plugin.ts`, author-chosen
`info`), 3 dropped as boolean latches rather than counters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 900e48935638a328b8aaa32ad5ddc29efa0f8dc3packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4f318de6644dc35ad759e3af51a0ff961d95ae9f — the merge of head 31f2ebced107a633411690c0638835d0ba6ea46e into base 900e48935638a328b8aaa32ad5ddc29efa0f8dc3, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4f318de6644dc35ad759e3af51a0ff961d95ae9f && git checkout 4f318de6644dc35ad759e3af51a0ff961d95ae9f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 900e48935638a328b8aaa32ad5ddc29efa0f8dc3 31f2ebced107a633411690c0638835d0ba6ea46e && git checkout -B drift-repro 900e48935638a328b8aaa32ad5ddc29efa0f8dc3 && git merge --no-ff 31f2ebced107a633411690c0638835d0ba6ea46e

node scripts/docs-audit/affected-docs.mjs --json 900e48935638a328b8aaa32ad5ddc29efa0f8dc3

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 20, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 20, 2026 18:11
@os-zhuang
os-zhuang enabled auto-merge August 20, 2026 18:11
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 20, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 20, 2026
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 20, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32406085715 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    FAIL  scripts/gen-sdui-manifest-collision.test.ts [ scripts/gen-sdui-manifest-collision.test.ts ]
      ↳ 失败原因: Error: Command failed: bash /tmp/sdui-collision-KwrNKK/harness.sh
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • scripts/gen-sdui-manifest-collision.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 41 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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

Development

Successfully merging this pull request may close these issues.

The logger?.error?.(…) spelling also carries two SUMMARY durability reports that no catch guards, so no gate can see them

2 participants