Skip to content

Measure the two unknowns blocking #9612/#9613, plus package-scoping viability - #9920

Merged
os-elon merged 1 commit into
mainfrom
claude/issue-9905-gate-cost-two-unknowns
Aug 19, 2026
Merged

Measure the two unknowns blocking #9612/#9613, plus package-scoping viability#9920
os-elon merged 1 commit into
mainfrom
claude/issue-9905-gate-cost-two-unknowns

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#9905

Three numbers and a written finding. No optimisation ships. The only code change is two
measurement modes on the existing #9851 instrument; no rule's surfaces, runtimeTypes or
surfaceReason is touched, and the shipped gate still receives the whole objects collection.

Measured at 2fb2e3fa4 (+ this commit), on one machine, --warmup 5 --iterations 30, median.
Every figure below names its stack shape (#9851): real = the shipped examples/app-showcase
declarations cloned to N; stub = synthetic 5-field objects with no expressions.

Premise checks first (rollback clause)

premiseverdict
object door costs ~285 ms @420 realholds — 276–332 ms median across 5 runs here (#9851 published 285.08; this machine runs ~1.1x slow)
the reachable closure might be the whole stackfalsified, favourably — it is 3 objects, constant in N
cross-package references might be pervasivefalsified, favourably0 of 191 edges in the shipped corpus

All three premises survive, so all three deliverables produce real answers rather than a stop.

Deliverable 1 — what per-rule scoping would actually buy

Written item: the shipped showcase_task_completed flow. Closure = object names appearing
anywhere in the item, expanded transitively through the included objects' fields[].reference
edges (a deliberate over-approximation, so the saving is a lower bound).

Real shape, --mode closure:

Nclosureclosure/Nfull gateclosure-fed gatesaving
21314.3%33.49 ms6.54 ms80.5%
10532.9%125.71 ms5.54 ms95.6%
42030.7%449.97 ms5.72 ms98.7%

The closure is showcase_task, showcase_project, showcase_account — the trigger object plus two
hops of lookup. It does not grow with N, because the tenant's other 417 objects are not reachable
from this flow. validateStackExpressions alone, real shape @420: 380.16 ms → 3.99 ms (98.9%).

The card's warning was right to insist on timing, and the answer is the opposite of the
worry.
The size ratio (0.7%) did not overstate the saving — it understated it as a fraction and
the residual is a fixed ~5 ms floor, not a proportional cost. And scoping at N=420 real beats
deleting the rule
(98.7% vs #9851's 83–85%), because narrowing the input shrinks what all seven
flow-door rules walk, whereas the ablation removed one rule's existence. At N=21 real the saving is
80.5%, i.e. below the deletion figure. So the honest range is 80–99%, rising with N — a
single "scoping saves X%" number does not exist.

Correctness, not just cost. A saving that changes the verdict is PR #7886's phantom-findings
failure, not a saving. Five probes at N=420 real, full stack vs closure — all agree, and four
carry a genuine finding rather than comparing zero to zero:

probefull stackclosureagree
clean flow, as shipped00yes
unresolvable field on the trigger object11yes
trigger object absent from the whole stack11yes
CEL that does not parse11yes
field belonging to a different object11yes

Structural reason this holds: for a flow write, findings about stored objects appear in both the
baseline and the candidate pass and cancel in the differential, so narrowing objects cannot change
what the write is charged for — as long as the closure contains every object the flow's own findings
resolve against.

Deliverable 2 — the object door, profiled

Real shape, N=420, whole-gate total 275.81 ms:

rulemsshare
validateRuleCompilability224.3581.3%
validatePresetComparands23.288.4%
validateFunctionalCompleteness7.072.6%
lintAutonumberFormats4.591.7%
validateSecurityPosture0.620.2%
validateRuleSchemaFormats0.240.1%
validateManagedApiMethods0.110.0%
unattributed (snapshot build + fingerprint diff)15.545.6%

Real shape, N=21 (total 17.70 ms): validateRuleCompilability 68.9%, validatePresetComparands
5.9%, unattributed 21.1% — at small N the gate's own fixed overhead is a fifth of the bill, and
that is stated rather than rounded away.

Stub shape, N=420 (total 7.68 ms): validatePresetComparands 30.9%, validateFunctionalCompleteness
25.5%, lintAutonumberFormats 15.3%, validateRuleCompilability 0.6%. Stub N=21 (total 0.40 ms)
is the same ordering. The driver vanishes on stub shape because stub objects declare no validation
rules — which is why the object door is a ~36x real/stub gap rather than a count effect.

Anti-vacuity control — a real ablation, not a harness filter.validateRuleCompilability was
taken off the object door via runtimeTypes, @objectstack/lint rebuilt, and the marker proven
live in dist/ with scripts/ablation-dist-preflight.mjs; rules dispatched went 7 → 6, so a stale
build could not have masqueraded as a saving. Restored byte-identical (sha256 verified) and rebuilt,
with the preflight re-run in --absent mode.

real @420rules dispatched
shipped332.08 ms (re-measured 303.89 after restore)7
ablated38.31 ms6

Removing the top contributor moved the total by 88.5% (87.4% against the re-measure) versus a
stated share of 81–88% across runs. The attribution method holds.

Findings

The two doors have disjoint drivers. The object door's cost is validateRuleCompilability
an object-only rule that appears in none of the profiles either #9612 or #9613 carries. Every
published concentration figure describes flow publishing, and none of it transfers.

validatePresetComparands is on both doors and is not the driver of either. It costs
23.28 ms on the object door and 23.34 ms on the flow door at N=420 real — the same absolute
work, because it walks the same objects collection either way. But that is 8.4% of the object
bill and 4.9% of the flow bill. It is genuinely one change benefiting both doors; it is worth
~23 ms of a ~276 ms and a ~476 ms bill, so it is a rounding error against either driver.

For reference, the flow door re-profiled with the same instrument, real @420 (total 476.29 ms):
validateStackExpressions 81.4%, validateReferenceIntegrity 5.9%, validateEmptyCombinators
5.1%, validatePresetComparands 4.9%, unattributed 2.6%. That reproduces the independently
published 70–85% band for the hot rule, which cross-checks the attribution method against a number
it did not produce.

Deliverable 3 — is package-scoped validation viable?

The boundary already exists in the data. Package membership is not a key on the object body
(packages/spec/src/data/object.zod.ts: the package-contribution kind is set via registerObject,
not on the schema). It is derivable from the object name: packages/spec/src/kernel/manifest.zod.ts
requires every object.name to be the namespace prefix plus a short name, and reserves the sys_
prefix for platform objects that apps may reference but never define. So a package-scoped gate needs
no new metadata and no new customer declaration — the boundary is in the name it already writes.

Cross-package reference fraction, over the shipped corpus (app-showcase 22 objects,
app-crm 6, app-todo 1 — 191 reference edges):

classedgesshare
same-package16988.5%
platform (sys_)2010.5%
cross-package00.0%
unresolved token (scanner artefact)21.0%

By edge kind: lookup 24 (16 same-package / 8 platform), master-detail 7 (all same-package),
flow-node-target 78 (all same-package), permission-grant 73 (61 / 12), sharing-rule-object 7 (all
same-package).

The crux — how wide is the platform dependency? This was the maintainer's own caveat, and it
resolves favourably. app-showcase names 3 platform objects directly (sys_user,
sys_business_unit, sys_business_unit_member); closed transitively over the platform objects' own
reference fields that reaches 4 of 44. app-todo reaches 3 of 44; app-crm reaches 0. The
dependency is narrow and shallow — it does not drag in the platform package.

Closure size, package + platform-closure, projected onto a packaged 420-object tenant:

packagingclosureof 420pessimistic (whole platform package)
19 packages x 22 objects266.2%15.7%
15 packages x 28 objects327.6%17.1%
7 packages x 60 objects6415.2%24.8%

Do the three cross-collection security rules survive it? Yes — and the 38-vs-4 hazard is on a
different collection.
PR #7886's phantoms came from narrowing permissions (a per-write
snapshot holding one set); package-scoping as proposed narrows objects. Measured one
collection at a time on a synthesized two-package tenant (package A = showcase, package B = the same
corpus re-namespaced, both shipping permission sets), with a positive control so a flat result is
known not to be a blind probe:

configurationphantomlost
positive control — permission write, permissions narrowed to the written item (the #7886 move)+80
permission write, objects scoped to package A, permissions whole00
object write, objects scoped to package A, permissions whole00
object write, objectsandpermissions both scoped to A00

The control reproduces the phantom mechanism at this corpus size (8 security-master-detail-ungranted
findings about package B's details). Every objects-scoping configuration reaches the whole-stack
verdict exactly. A deliberately cross-package master-detail — a shape the corpus scan counts zero
of — also produced an identical verdict under scoping.

Two mechanisms explain it: the rule self-silences when permissionSets is empty, and sibling-derived
findings cancel in the gate's differential. The rules that need siblings need them in permissions,
which package-scoping objects does not touch.

Verdict

Cross-package references are rare (measured: zero) and the platform dependency is narrow (4 of
44)
, so on this evidence package-scoping is the stronger route: it uses a boundary the customer
already declares, it collapses the realistic N to 6–16% of the tenant's stack, and it manufactures no
false positives in the family that has produced them before.

⚠️The honest limit of this number. The shipped corpus contains no multi-package tenant
three standalone example apps that are never installed together. "Zero cross-package references" is
therefore a necessary condition met, not an observation of a real integrated multi-package tenant.
Nothing here measures how often two packages a customer installs together deliberately reference each
other. If that number is what the decision turns on, it needs a corpus this repo does not have.

What ships

scripts/bench/runtime-publish-gate.bench.mts gains --mode per-rule and --mode closure, both
reusing the gate's own exported buildRuntimeWriteSnapshots / runtimeAuthoringRulesFor rather than
mirroring the construction. --mode total output is unchanged. --mode closure narrows what the
rules are handed inside that script only; its closure deriver is a measurement over-approximation,
explicitly not a proposed implementation.

Deliverable 3's corpus scan and security probe were throwaway prototypes, as the card permits; their
method is stated above in full (name-prefix boundary, fields[].reference transitive expansion,
one-collection-at-a-time with a positive control).

No changeset — the only changed file is a bench script in the root, not a published package, and
nothing user-visible changes. skip-changeset applied.

Gates

Union re-derived with node scripts/pm/dispatch-gates.mjs (no path list passed) at 3171ee40b,
which is the final commit:

  • pnpm check:cross-package-test-inputs — OK (33 self-test cases; 12 packages read outside themselves, all declared)
  • pnpm check:nul-bytes — OK (6285 files, no raw control bytes)
  • pnpm check:type-check-debt — both legs OK. Run per the card's instruction, which the deriver
    structurally cannot surface: scripts/** sits in the ROOT program, defined by exclusion, so no
    path literal names it. Full package closure built first as the gate demands. 33 ledger entries
    re-measured, 1926 raw errors, none above its recorded number, surplus: none
    — the root entry did
    not move.

Generated by Claude Code

…9905)
Two measurement modes on the existing #9851 instrument, so the numbers
#9905 asks for are re-runnable instead of living only in a PR body.
--mode per-rule attributes the whole-gate total across the rules the
door dispatches, timing each over BOTH gate passes and
printing the UNATTRIBUTED remainder rather than
rounding it away.
--mode closure times the gate with the objects collection narrowed to
the written item's transitive reference closure, and
re-checks that the differential verdict is unchanged.
Both reuse the gate's own exported buildRuntimeWriteSnapshots /
runtimeAuthoringRulesFor rather than mirroring the construction. No rule's
surfaces, runtimeTypes or surfaceReason is touched and the shipped gate
still receives the whole collection; --mode closure measures a
hypothetical inside this script only. Default output is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
@os-elonos-elon added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 19, 2026 — with Claude
@os-elonClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT on substance; enqueue held on the two jobs still running

Reviewed against GitHub. Lint & Repo Gates and TypeScript Type Check are still in_progress; everything else is success or skipped. ⛔ Not flipping ready until each reports its own conclusion — holding on the individual job, not the aggregate, is what caught #9853's ratchet regression this round, and I am not abandoning it on the round where it paid.

Fences held

One file. scripts/bench/runtime-publish-gate.bench.mts only. No rule's surfaces / runtimeTypes / surfaceReason touched; the shipped gate still receives the whole collection. --mode closure's docblock says in as many words that it measures a hypothetical, narrows only inside the script, and that its closure deriver is "a deliberately generous over-approximation written to bound the saving, NOT a proposed implementation." That is exactly the line the card drew, stated where the next reader will hit it.

The over-approximation direction is right and is argued rather than asserted: the deriver seeds from any string equal to an object name, so it can only be too large, which makes the measured saving a lower bound — the safe direction for a number that argues for narrowing.

Divergence — and the check I added this round FIRED

Merge base 2fb2e3fa4; main advanced 12 commits.

proberesult
commits touching the PR's file0
commits touching the derivation source (packages/lint/src/{runtime,runtime-gate,authoring-rules}.ts)⚠️1
positive control on a file main did move1 — probe can return non-zero
files this branch changes1

The second row is the check this seat adopted from #9847 (for a derived artifact, measure divergence on the SOURCE of the derivation, not only the edited file). It fired. I looked, and it clears:

818c27ce4 (#9600 / #9908) changes the where field of three ADR-0120 uniqueness rules and their return type to LocatedLintIssue. It touches neither export this bench imports (runtimeAuthoringRulesFor, buildRuntimeWriteSnapshots), no runtimeTypes, no surfaces. Door membership on current main re-derived with the anchored probe: object 7, flow 7 — matching what was measured. The one contact point is that closureMode's verdict fingerprint includes f.where; that is a full-vs-closure comparison within one build, so both legs move together and the "verdict unchanged" claim is unaffected.

A check that fires and then clears on inspection is the check working, not noise.

⭐ My premise was falsified, and that is the most valuable thing in this report

The dispatch told you, in my words: "narrowing the resolution universe is exactly the action that manufactured those false positives."That was wrong, and you established why.

PR #7886's 38-vs-4 phantoms came from narrowing permissions — the docblock says the snapshot held "exactly ONE permission set". Package-scoping narrows objects. Different collection, different mechanism. I conflated them when scoping the card, and that conflation was the single biggest reason the maintainer's proposal looked like it was arguing against measured evidence.

What makes the refutation stick is the positive control: a permission write with permissions narrowed to the written item reproduces the mechanism at +8 phantoms on this corpus. So the flat zeros in the other three configurations are a measurement, not a blind probe. Without that row the result would have been unfalsifiable and I would have sent it back.

⭐ And the card's own warning was right to insist — but the surprise ran the other way

The card said: "Do not stop at the size ratio. A closure that is 5% of the stack does not automatically mean 95% saving — the rule's cost may not be linear in input size. Time it."

Timed, the ratio understated the saving: the residual is a fixed ~5 ms floor, not a proportional cost. And the consequence is the finding I did not anticipate — scoping at N=420 real beats deleting the rule outright (98.7% vs #9851's 83–85%), because narrowing the input shrinks what all seven flow-door rules walk while the ablation removed one rule's existence. At N=21 it is below that figure (80.5%).

⇒ The honest answer is a range, 80–99% rising with N, and the report says so instead of quoting the best number. That is the discipline #9859 and this card both demanded, applied against the dev's own most quotable result.

The finding that changes the shape of #9612/#9613

The two doors have disjoint drivers. The object door is validateRuleCompilability at 81.3% — an object-only rule appearing in no profile either parked card carries. Every published concentration figure describes flow publishing and none of it transfers. validatePresetComparands sits on both at the same absolute cost (23.28 ms object / 23.34 ms flow @420 real) but is 8.4% / 4.9% — genuinely one change for both doors, and a rounding error against either driver. Both statements are precise about what they do and do not license.

Attribution is controlled properly: unattributed printed even when uninteresting (5.6% @420, 21.1% @21 — stated, not rounded away); a real ablation with a unique marker, rebuild, ablation-dist-preflight proving the marker live in 4 built files, rules dispatched 7→6 so a stale dist cannot masquerade as a saving, 88.5% observed against a stated 81–88% share, restore byte-identical by sha256 with the preflight re-run --absent. And a method cross-check I did not ask for: the flow door re-profiled with the same instrument gives validateStackExpressions81.4%, reproducing the independently published 70–85% band the method did not produce.

The limit is stated by the author, unprompted

The shipped corpus contains no multi-package tenant — three standalone example apps that are never installed together. "Zero cross-package references" is therefore a necessary condition met, not an observation of a real integrated multi-package tenant.

That belongs in the record exactly as written. A 0/191 that reads as proof would have been the more impressive and less true report.

Two decisions go to the maintainer, not to me

Both open questions are genuinely the maintainer's — one is how to close the corpus gap above, the other is whether the object door gets re-scoped onto its own driver. Taking them there now; ⛔ not self-adjudicating either.


Generated by Claude Code

@os-elonClaude

Copy link
Copy Markdown
CollaboratorAuthor

Ready flipped, enqueued (squash auto-merge)

TypeScript Type Check — the last job outstanding, and the one carrying the 334 s check:type-check-debt re-measure — reports success at 3171ee40b. Lint & Repo Gates green; everything else success or skipped. Held on that job's own conclusion, never the aggregate.

Divergence re-measured against the current main at enqueue time rather than reused from the review:

proberesult
main commits since merge base 2fb2e3fa413 (was 12 at review)
of those, touching scripts/bench/runtime-publish-gate.bench.mts0
touching the derivation source (packages/lint/src/{runtime,runtime-gate,authoring-rules}.ts)1 — still only 818c27ce4, assessed and cleared
runtimeAuthoringRulesFor / buildRuntimeWriteSnapshots still exportedboth present
door membership on current main, anchored probeobject 7 · flow 7 — matches what was measured
positive control on a file main did move1
files this branch changes1

The last three rows are the ones that matter for a measurement PR. This branch's numbers were taken on a tree without 818c27ce4; re-deriving door membership and export presence on today's main confirms the readings still describe the shipped registry, so nothing in the report went stale while it sat in review.

Tracking to MERGED and verifying on the artifact (--mode per-rule / --mode closure present; control: --mode total output unchanged).


Generated by Claude Code

Merged via the queue into main with commit ae555f4Aug 19, 2026
24 checks passed
@os-elon
os-elon deleted the claude/issue-9905-gate-cost-two-unknowns branch August 19, 2026 08:51
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Measure the two unknowns blocking #9612/#9613: what per-rule scoping would actually buy, and where the object door's 285 ms goes

2 participants

@os-elon@claude