Uh oh!
There was an error while loading. Please reload this page.
fix(cli): report only what the hook-body build gates establish - #10912
Conversation
Three defects on the hook-body build-gate surface, all reporting and reachability — the enforcement net itself held throughout. - Default os build recorded bodyExtractionWarnings and printed them nowhere, so a forbidden pattern in a hook body was a silent exit-0 success. Surface them on stdout and in --json under a new bodyExtractionWarnings key. Exit code unchanged. - The require() refusal reason could never match on a TS config, because esbuild rewrites require( to __require( before String(fn) runs. Match both spellings under the one reason. Accept behaviour unchanged. - The // @capabilities directive reaches the extractor from no ordinary authoring shape through os build. Document the measured reach and pin it with an os build-level test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
📓 Docs Drift CheckThis PR changes 1 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 36e878f452fcb03000db4636b297bb5e825837ba && git checkout 36e878f452fcb03000db4636b297bb5e825837ba
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 58563be069a70d09c4d9cc5f522bb27c5f8a78b2 4edd41f194e87cf1c440466d41594f754246338f && git checkout -B drift-repro 58563be069a70d09c4d9cc5f522bb27c5f8a78b2 && git merge --no-ff 4edd41f194e87cf1c440466d41594f754246338f
node scripts/docs-audit/affected-docs.mjs --json 58563be069a70d09c4d9cc5f522bb27c5f8a78b2
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32508204346 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Part of #10678
Three defects on the hook-body build-gate surface, each ruled separately by triage. All three are one shape: a gate reporting something it never established. The enforcement net itself held throughout and is unchanged — no forbidden body ever shipped as
body.source, and every forbidden or free-identifier hook is still refused under--strict-body, at the same exit codes as before. This is reporting and reachability.Part ofrather thanFixes: defects 1 and 3 each leave a fenced maintainer decision open (below), so the card should not close on merge.All three reproduced at head before any edit
Anchors re-derived at head (
7c02a4529c), measured by spawning the real CLI againstmkdtempprojects:@capabilitiesdead throughos buildapi.read api.writeshipped"capabilities": []require()reason deadhandler references identifier(s) not in scope at runtime: __requirefetch()in a hook body: exit 0, no output at allDefect 3 — surface the recorded warnings (the highest-value one)
bodyExtractionWarningswas already recorded bylowerCallablesand printed nowhere.compile.tsnow prints it on the default path, naming the hook and the pattern, and pointing at--strict-body;--jsoncarries it under a newbodyExtractionWarningskey.Separate key from
warningson purpose:warningscarries author-time rule advisories in the shapeos validate --jsonalso reports ({where, message, rule, path, hint}), and these are a different record ({origin, reason}). Folding them together would have broken that shared shape for every consumer reading one shape from either command. It is an empty array on a clean build, so CI can read it unconditionally — pinned by a control test.Exit code unchanged (still 0). Flipping the default build to hard-fail would change what
os buildaccepts — fenced, raised in the report instead.Defect 2 — the
require()reason now fires on the real pathesbuild's ESM interop shim rewrites
require('node:os')to__require("node:os")beforeString(fn)runs, so/\brequire\s*\(/could never match on a TS config. Now/\b(?:__)?require\s*\(/, and the reason explains the rewrite so__requirein the dumped source is not a mystery identifier.Accept behaviour unchanged, and tested as such: the body was already refused (via the #1876 free-identifier gate), already bundled, at the same exit codes — 1 under
--strict-body, 0 by default. Only the wording moved. The widening cannot reach an author's ownmyrequire((no word boundary inside it); a test pins that.The sibling
import()pattern was checked for the same class and is fine — esbuild preserves theimport(spelling in its rewrite, so that reason still fires. Nothing to file.Defect 1 — docs-truth route, and the premise came back stronger
Triage said "document that the directive only survives pre-bundled JS." Measured, it survives nothing that an app author would write.
bundleRequireruns esbuild on every config regardless of extension, and esbuild strips//line comments before the handler is a runtime value:objectstack.config.tsobjectstack.config.jsobjectstack.config.mjs./handlers.jsSo the docs do not point readers at a
.jsfile that also does not work — that would have been a fresh false claim of exactly the kind this card is about. They state the reach as measured and point atbody.capabilities(data, not a comment) as the escape hatch that does survive — itself measured, not assumed.The
os build-level test, and why it had to be onetest/hook-body-build-reach.e2e.test.tsspawns the real CLI (bin/run-dev.js+ tsx, thevalidate-top-level-strict.e2e.test.tspattern) and reads the artifact and the shell's exit code.The existing extractor unit tests could not have caught any of this: they feed raw JS function literals, which keep their comments and their
require(spelling because nothing transformed them. That is whyhonours explicit @capabilities overridehas passed for as long as it has existed while the override never once worked throughos build. A unit test over the extractor alone would have restated exactly that false confidence.The defect-1 assertions pin reach, not endorsement — the file says so, and says that if the fenced decision lands they must be rewritten alongside the docs page, not deleted.
Reverse-verification
Both fix legs were reverted on top of the commit and re-run; the restore leg was re-run to green. Predicted direction was red, and red is what happened, each for the right reason:
Restore leg:
Test Files 2 passed (2) · Tests 26 passed (26).No rebuild was needed for either leg and none is claimed:
bin/run-dev.jsruns the CLI fromsrc/through tsx — its own header says it exists "so they do not depend onpackages/cli/disthaving been built" — and the unit test imports fromsrc. Nothing in this ablation resolves throughdist.The defect-1 reach test stayed green under ablation, correctly: it pins pre-existing measured behaviour and has no code mutation to be sensitive to. Its regression sensitivity is to a future change in reach.
Verification
Head is
4edd41f194. Being exact about which tree each run saw, since two of them predate the final amend:4edd41f194, re-run on the final head. Derived withnode scripts/pm/dispatch-gates.mjsagainst the real change set — no hand-built path list — which is 27 families, pluscheck:nul-bytesand the two convention-triggered families the residue names for new test files (check:engine-double-contract,check:where-matcher).os-verify-lock: VERDICT command-exit 0.check:type-check-debt— the ratchet half — run afterturbo run buildover the workspace closure (70 successful, 70 total), aslint.ymldoes. Its own verdict line:check-type-check-coverage: OK — 64/77 workspace packages type-checked (plus the root), 13 in the DEBT ledger.TCDEBT_EXIT=0.packages/clifull suite: 147 files / 1622 tests passed, 0 failed, pluspnpm --filter @objectstack/cli typecheckclean. (The--file filter was not honoured, so the whole package ran — more coverage than intended, not less.)The suite and ratchet runs both saw commit
5b09f67a9e; the amend to4edd41f194added one prose line tohook-bodies.mdxand changed nothing else. That line is in no tsc program and in no test, so neither reading moves — and the docs families that can see it (check:doc-anchors,check:doc-authoring,check:doc-frontmatter,check:role-word,check:docs-audit-scope) were re-run on the amended tree before the gate batch, and again inside it.check:doc-anchorsis what confirms the new#capability-inferencefragment link resolves.Regression set from the card (capability inference, the
crypto.hashnon-inference of #4391, the #1876 bundle fallback, all-body-only skip plus.mjscleanup) is green inside that full-suite run. No threshold moved, no baseline raised, nothing skipped or quarantined.Two fenced decisions, not taken here
Both are maintainer calls about the published surface; both are in the report's
open_questionswith the evidence.@capabilitiesget a real authorable surface, or be retired? Today it is a documented feature reachable from no ordinary authoring path. Documenting it as unreachable is stable but leaves a directive in the docs that does nothing. Adding a config property expands the public surface.os buildaccepts.Nothing under
content/docs/releases/**was touched; the changeset is the release-notes input.Generated by Claude Code