fix(devx): select the envelope-caller census on scripts-only PRs via the cross-package union - #15709
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…velope-census-pr-side
…-only PR selects it The census walks the whole repo at module load but was selected only through the dependency graph, so a scripts-only diff could not redden it before the merge queue. Declare `scripts/**` on @objectstack/client in the existing cross-package test-input manifest; Layer A (--union-into) then pulls the package into the PR-side affected set and Layer B (turbo.json) moves the task's cache hash. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
…velope-census-pr-side
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
|
ACCEPT — PM seat Reviewed against the three-dot diff at What the diff does — the WHEN-it-runs axis only: Measured by this seat on the PR head and on spec and cli on both sides are the positive control that the union was already working; client is the delta this card asked for. The census suite itself ( Fences held: three files, each justified; Generated by Claude Code |
Fixes #15608
The envelope-caller census is whole-repo in what it reads (
const CENSUS = scanCallSites(REPO_ROOT)at module level) but was package-scoped in when it runs: CI reached@objectstack/clientonly through the dependency graph, so ascripts/**-only diff selected it for nothing and the census could not report until the merge queue. That is what ejected five PRs.This is the WHEN-it-runs axis only. ⛔ What the census COUNTS is untouched — #13874's suspension of the literal/context distinction stands exactly as written, and no literal, no
scanCallSites, no ledger row is changed here.Mechanism — the existing union, not a new derivation
Neither option A (always-run) nor option B (a bespoke extension-based rule). The repo already has a mechanism for exactly this defect class, built for #7802, and the census simply had not declared into it.
scripts/check-cross-package-test-inputs.mjsstates its own contract:So the change is one declared glob plus its Layer B mirror:
scripts/cross-package-test-inputs.mjs—@objectstack/clientdeclaresscripts/**, with aheldBywitness naming the census as the test that reads it.turbo.json— the Layer B mirror the contract above requires:$TURBO_ROOT$/scripts/**on@objectstack/client#test. This file is edited only because it is where Layer B reads declarations;--verifyfails without it.packages/client/src/envelope-caller-census.test.ts— docblock only. Its header sentence "invisible to every local gate ascripts/**edit derives" is no longer true, so it is quoted and corrected rather than left standing.No ci.yml change is needed. Layer C (
check-ci-filter-parity) already covers this glob:@objectstack/specdeclaresscripts/**verbatim, so ci.yml'scrosspkgfilter already starts thetestjob on ascripts/**diff. Verified, not assumed — see below.Why
scripts/**and not the census's whole read radius. That difference is a price, not an oversight. Layer B mirrors every declared glob into the task's cache inputs, so declaringpackages/**would re-run the client suite on virtually every commit — the bound that test's header has recorded as declined since #13079.scripts/**is the root a quoted example actually lives in (refusal messages, usage banners, embedded fixtures) and the root the incident came from. What stays uncovered stays recorded in the test's header, unchanged.Before / after, on a scripts-only change set
Run exactly as ci.yml's
testjob invokes it, against a synthetic affected set ({"packages":{"count":0,"items":[]}}— whatturbo ls --affectedreturns for a scripts-only diff) and a one-line changed-file list,scripts/check-nul-bytes.mjs:BEFORE (declaration ablated away):
AFTER:
@objectstack/specand@objectstack/cliappearing in both are the positive control: the union mechanism was working the whole time; the census had simply never declared into it.Ablation
Trap-guarded (
trap restore EXIT INT TERM), absolute paths, on-disk proof in both directions. No rebuild leg exists in this resolution path and that is checked, not assumed: the gate imports the manifest by relative specifier (import { CROSS_PACKAGE_TEST_INPUTS } from './cross-package-test-inputs.mjs') and node runs both from source — there is nodist/between the edit and the verdict.The glob is declared twice in the file (spec's and client's), so the anchor is resolved by line, relative to the client entry header — a bare text match was tried first and the guard refused it as non-unique rather than silently ablating spec's.
065e771576f97ff331ad820603e2ce53d6a25630; after mutationc75726f3cbc4c21d16b9c1cc4a9e383d5cb1198b(differs ⇒ the mutation reached disk); occurrences 2 → 1, andgit diffshows exactly- 'scripts/**',.count=2(above), and Layer B--verifyexits 1, naming both limbs —@objectstack/client names path(s) no declared glob coversand@objectstack/client has heldBy witness(es) keyed to glob(s) it does not declare. The wiring is pinned in two independent places.065e771576f97ff331ad820603e2ce53d6a25630(equal to HEAD),git diff HEADempty, occurrences back to 2, union back tocount=3,--verifyexit 0.Measured cost
pnpm --filter @objectstack/client exec vitest run --maxWorkers=2 src/envelope-caller-census.test.ts, under the shared verify lock (VERDICT command-exit 0 · held the lock 14s):check:*work on the same cores. The ratio survives contention: import 11.53 s vs tests 215 ms, ~54:1 — 97% of the wall is the import-time whole-repo scan, reproducing the card's 92% / 10.85–11.85 s import vs 174–210 ms tests.⇒ This is what makes the union the right shape rather than option A. A cheaper trigger does not make the gate cheap, it makes it earlier — so the ~12 s is bought only on PRs that touch
scripts/**, not on every PR (A) nor on nearly every PR (B).Census verdict unchanged: 20 tests pass, and the ratchet's live identity
ledgerTotal === CENSUS.sites.lengthstill holds at 29 enumerated sites — 18PAYLOAD_DEPENDENT+ 10RESULT_INSENSITIVE+ 1NOT_SDK+ 0ENVELOPE_DEPENDENT, 28 of them SDK sites.⛔ Not incremental scanning. If ~12 s on a
scripts/**PR is judged too much, that is a separate card, as the ruling says.Verification
Every command below ran on the final head
0f13ca46f, exit code captured before any pipe, verdict lines quoted from the gate's own output.The gate family was derived mechanically, not guessed:
node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack(exit 0, change set = the 3 paths, no stale-tree warning). All 61 commands ran; 58 exit 0.Plus a
grep -naPcontrol-byte self-scan over the three edited files: no hits.NOT MEASURED — by name, and why
Three of the 61 exited non-zero, all the same class:
PREREQUISITE NOT METin a fresh worktree with nodist/. None is a red, none is a pass, and none is reachable from this diff (which touches a docblock, a manifest.mjsandturbo.json— nothing that can move built output). CI builds before these steps.pnpm --filter @objectstack/spec run check:skill-examples— exit 1: "packages/spec/dist holds no .d.ts declarations — the package is not built".pnpm check:dual-build-cjs-loads— exit 3: "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/", 103 packages listed.pnpm check:type-check-debt— exit 3: "33 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk".Also NOT MEASURED, and named by the derivation itself: the six families whose argv carries a
RUNNER_TEMPvariable with no value outside a CI run — includingcheck-cross-package-test-inputs --union-intoin its literal ci.yml spelling. That step is measured here instead through the synthetic before/after above, which is the same code path with the same two arguments supplied by hand.The always-runs tail (workflows with no path filter) is CI's, not derivable locally.
Notes
turbo.json.skip-changesetapplied..github/workflows/lint.ymland rootpackage.jsonwere not touched (held by queued PR feat(devx): a docs gate binds each CLI transcript in content/docs to the registry it quotes, so a derived count cannot drift silently #15651); neither was needed..github/workflows/ci.ymlwas not touched either — it is unnecessary once Layer C is checked, which is the whole point of preferring the existing mechanism.🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code