Skip to content

fix(devx): select the envelope-caller census on scripts-only PRs via the cross-package union - #15709

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-15608-envelope-census-pr-side
Sep 5, 2026
Merged

fix(devx): select the envelope-caller census on scripts-only PRs via the cross-package union#15709
baozhoutao merged 4 commits into
mainfrom
claude/issue-15608-envelope-census-pr-side

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

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/client only through the dependency graph, so a scripts/**-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.mjs states its own contract:

A package whose tests read outside itself declares that radius ONCE, in CROSS_PACKAGE_TEST_INPUTS [...] and both layers are driven from it:
Layer A: --union-into adds the declaring package to the shard's package set when the diff touches its declared globs.
Layer B: --verify requires turbo.json to carry a matching PKG#test task whose inputs include the same globs as $TURBO_ROOT$/... entries, so the cache hash moves with them.

So the change is one declared glob plus its Layer B mirror:

  • scripts/cross-package-test-inputs.mjs@objectstack/client declares scripts/**, with a heldBy witness 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; --verify fails without it.
  • packages/client/src/envelope-caller-census.test.ts — docblock only. Its header sentence "invisible to every local gate a scripts/** 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/spec declares scripts/** verbatim, so ci.yml's crosspkg filter already starts the test job on a scripts/** 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 declaring packages/** 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 test job invokes it, against a synthetic affected set ({"packages":{"count":0,"items":[]}} — what turbo ls --affected returns for a scripts-only diff) and a one-line changed-file list, scripts/check-nul-bytes.mjs:

BEFORE (declaration ablated away):

Cross-package scans pulled into this run because the diff touched their declared inputs:
  + @objectstack/spec  (declared glob matched scripts/check-nul-bytes.mjs)
  + @objectstack/cli  (declared glob matched scripts/check-nul-bytes.mjs)
    package list: count=2 items=["@objectstack/spec","@objectstack/cli"]

AFTER:

Cross-package scans pulled into this run because the diff touched their declared inputs:
  + @objectstack/spec  (declared glob matched scripts/check-nul-bytes.mjs)
  + @objectstack/cli  (declared glob matched scripts/check-nul-bytes.mjs)
  + @objectstack/client  (declared glob matched scripts/check-nul-bytes.mjs)
    package list: count=3 items=["@objectstack/spec","@objectstack/cli","@objectstack/client"]

@objectstack/spec and @objectstack/cli appearing 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 no dist/ 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.

  • HEAD blob 065e771576f97ff331ad820603e2ce53d6a25630; after mutation c75726f3cbc4c21d16b9c1cc4a9e383d5cb1198b (differs ⇒ the mutation reached disk); occurrences 2 → 1, and git diff shows exactly - 'scripts/**',.
  • Ablated: the union drops to count=2 (above), and Layer B --verify exits 1, naming both limbs — @objectstack/client names path(s) no declared glob covers and @objectstack/client has heldBy witness(es) keyed to glob(s) it does not declare. The wiring is pinned in two independent places.
  • Restored: blob back to 065e771576f97ff331ad820603e2ce53d6a25630 (equal to HEAD), git diff HEAD empty, occurrences back to 2, union back to count=3, --verify exit 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):

 Test Files  1 passed (1)
      Tests  20 passed (20)
   Duration  11.92s (transform 270ms, setup 0ms, import 11.53s, tests 215ms, environment 0ms)

⚠️ Shared-box seconds, not idle-box — the lock excludes other locked runs, not the unlocked 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.length still holds at 29 enumerated sites — 18 PAYLOAD_DEPENDENT + 10 RESULT_INSENSITIVE + 1 NOT_SDK + 0 ENVELOPE_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.

OK: 27 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
All 117 self-test cases passed.                                     (check:cross-package-test-inputs)

OK: all 149 declared cross-package glob(s) (102 unique) are covered by `core` or `crosspkg`, every
`crosspkg` entry still covers one, and the `test` job's `if:` still names both filters.
                                                                    (check-ci-filter-parity — Layer C)

1445 cases pass                                                     (check:pm-dispatch-gates)

check-nul-bytes: OK (scanned 7588 text file(s) ... no raw ASCII control bytes).   (check:nul-bytes)

OK: 29 package-scoped and 8 generic turbo task(s) judged against 79 workspace package(s) ...
                                                                    (check:turbo-task-graph)

Plus a grep -naP control-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 MET in a fresh worktree with no dist/. None is a red, none is a pass, and none is reachable from this diff (which touches a docblock, a manifest .mjs and turbo.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_TEMP variable with no value outside a CI run — including check-cross-package-test-inputs --union-into in 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

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…-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
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 5, 2026
@github-actions github-actions Bot added the size/s label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 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 581d8f84c660997deec0075c7e03bc98be0a7f10packageMentionDocs.

@github-actions github-actions Bot added the tests label Sep 5, 2026
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff at 0f13ca46f (3 files, +67/−12), not the report. Closing keyword: Fixes #15608 only.

What the diff does — the WHEN-it-runs axis only: @objectstack/client declares scripts/** as a cross-package test input in scripts/cross-package-test-inputs.mjs with a checked heldBy witness naming envelope-caller-census.test.ts, mirrored into turbo.json as $TURBO_ROOT$/scripts/** on @objectstack/client#test (the only reason turbo.json is touched: it is where the union gate's Layer B reads declarations, and --verify fails without it). The census test's docblock stops claiming the suite is "invisible to every local gate a scripts/** edit derives" and records that packages/** stays undeclared as a priced bound. scanCallSites and everything the census COUNTS are untouched — #13874 stays suspended. No ci.yml/lint.yml/root package.json change (Layer C already covers the glob via @objectstack/spec's identical declaration). Neither option A nor a bespoke derivation: the repo's existing union mechanism, declared into.

Measured by this seat on the PR head and on origin/main (plain node, detached worktrees; readings copied from the run output):

node scripts/check-cross-package-test-inputs.mjs --self-test   EXIT=0   All 117 self-test cases passed.
node scripts/check-cross-package-test-inputs.mjs --verify      EXIT=0   OK: 27 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
scripts-only change set (changed = scripts/check-nul-bytes.mjs; turbo ls --affected = 0 packages):
  origin/main:  --union-into → 2 packages  [@objectstack/spec, @objectstack/cli]
  PR head:      --union-into → 3 packages  [@objectstack/spec, @objectstack/cli, @objectstack/client]   "+ @objectstack/client (declared glob matched scripts/check-nul-bytes.mjs)"

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 (vitest, imports typescript) is NOT MEASURED here — the dev's reading: 20/20, 29 sites, import 11.53 s vs tests 215 ms (the card's 92 % reproduced); CI is the arbiter.

Fences held: three files, each justified; check-governed-merges.mjs --test0 of 3 path(s) hit the register; no other open PR holds them; git merge-tree origin/main clean; skip-changeset (nothing published). Flip + arm once both required jobs read success (Lint & Repo Gates in progress at review time).


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 5, 2026 04:07
@baozhoutao
baozhoutao enabled auto-merge September 5, 2026 04:07
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 76ba5b7 Sep 5, 2026
39 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-15608-envelope-census-pr-side branch September 5, 2026 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants