Skip to content

feat(lint,metadata-protocol): judge a package write against its own closure - #10058

Merged
os-elon merged 4 commits into
mainfrom
claude/issue-9612-publish-gate-package-closure
Aug 20, 2026
Merged

feat(lint,metadata-protocol): judge a package write against its own closure#10058
os-elon merged 4 commits into
mainfrom
claude/issue-9612-publish-gate-package-closure

Conversation

@os-elon

@os-elon os-elon commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Fixes #9612

Implements the re-scoped card in comment 5346052895 — not the issue body, whose menu of three routes (size threshold / caching + invalidation / per-rule scoping) it supersedes. Sibling #9613 is folded in.

This is not "make publish faster". It is validate a package write against the package's own closure. The latency numbers below are how the closure is confirmed correct; they are not the point. Maintainer, verbatim and standing: 「目前也没有相关的真实用户」 · 「objectstack cloud 还没有正式上线,简化开发」. No tenant is waiting on this.

The ruling the whole change hangs on:

大客户(420 个对象),就不应该出现在一个软件包中啊,这就是划分软件包的价值。
客户开发开发,校验是否也应该基于软件包
当然这里面要考虑系统对象


FIRST DELIVERABLE — the object door, measured

The card required this before any implementation, and forbade assuming the flow door's 80–99% transfers. Every figure below names its shape and its N.

Instrument scripts/bench/runtime-publish-gate.bench.mts. Registry state proved live in the built artifact before any number was read:

$ node scripts/ablation-dist-preflight.mjs @objectstack/lint 'validateRuleCompilability'
✓ marker present in 6 built files (plus 4 sourcemap hits, not counted)
  -- the ablation is live in the artifact the suite consumes.

1. The object door's driver, reproduced (--mode per-rule --type object)

rules dispatched (7): validateFunctionalCompleteness, validateManagedApiMethods, validatePresetComparands, lintAutonumberFormats, validateSecurityPosture, validateRuleCompilability, validateRuleSchemaFormats

shape / N whole gate validateRuleCompilability validatePresetComparands
real 21 18.02 ms 13.34 ms (74.0%) 1.10 ms (6.1%)
real 105 79.69 ms 61.28 ms (76.9%) 6.42 ms (8.1%)
real 420 280.51 ms 226.40 ms (80.7%) 27.74 ms (9.9%)
stub 420 7.81 ms 0.04 ms (0.5%) 1.96 ms (25.2%)

The card's 81.3% of 275.81 ms reproduces at 80.7% of 280.51 ms. validatePresetComparands is 9.9% here — a rounding error against the driver, and the card was right to fence the scope away from it.

Why it narrows, from the source rather than from the number: validateRuleCompilability reaches objects through walkObjectValidationRules(stack), which iterates stack.objects whole and hands every json_schema / format rule on every object to a real ajv compile — twice per publish, once per differential pass. It is not reading "the object being written"; it is reading the tenant.

2. Narrowing by package closure, the shipped path (--mode package)

A new bench mode. Unlike --mode closure, whose docblock declares it a HYPOTHETICAL, this one calls the shipped narrowObjectsToPackageClosure on a packaged seed, so the number describes shipped behaviour. Seed: 22 objects per package, plus one platform object owned by a package nobody declares.

Closure = own package + declared dep + platform/system + unpackaged = 45 of 421 (10.7%) — inside the card's 6.2–15.2% reference band.

door shape / N full package closure saving
object real 22 30.43 ms 27.17 ms 10.7% (closure = 100%, nothing to narrow)
object real 106 85.52 ms 42.66 ms 50.1%
object real 421 312.94 ms 41.89 ms 86.6%
object stub 421 22.19 ms 13.62 ms 38.6%
flow real 22 42.84 ms 35.71 ms 16.6%
flow real 106 160.14 ms 60.58 ms 62.2%
flow real 421 578.56 ms 62.28 ms 89.2%
flow stub 421 25.86 ms 4.46 ms 82.8%

The object door does narrow — and it was not safe to assume so: it narrows by a different mechanism (one rule compiling every object's schemas) than the flow door (seven rules walking every object's expressions). The residual is a floor, not a proportion: the closure leg costs ~42 ms at N=106 and ~42 ms at N=421.

⚠️ Below N=45 the closure is the whole tenant and the saving is nil by construction, which is the correct behaviour, not a gap: a tenant that small is one package.

3. Differential verdict — and one honest caveat

Semantically UNCHANGED at every N, both shapes, both doors.

⚠️ The raw comparison including the finding path reports +1 / -1 on the object door whenever narrowing occurs. That is not a verdict change: object-door findings carry an index-based path (objects[417].sharingModel), narrowing moves the written item's index, and where already carries the object name. Both readings are printed by the bench rather than the inconvenient one being hidden — the index really is wire-visible in issues[].path, and it is an index into a snapshot array no caller has ever seen.

4. Positive controls — and where they are silent

control flow door, real 421 object door, real 421
A — written item's own package dropped +1 phantom finding no delta
B — system objects dropped −4 findings lost no delta

Control A reproduces the #7886 mechanism on objects: flow-trigger-unknown-object on a target that is really there. Control B shows the "system objects unconditionally IN" limb is load-bearing in the other direction — omitting it loses real findings.

⚠️ Both controls are silent on the object door, and that is a finding, not a pass. Source-verified: none of the seven object-door rules builds a name→object index or resolves a reference across objectsvalidateSecurityPosture crosses objects × permissions and objects × books, never objects × objects; lintAutonumberFormats's only Map is per-object field metadata. The object door has no objects×objects coupling at all, so on that door a verdict-equivalence assertion cannot fail for closure reasons. Stated plainly because an unfalsifiable green must not be read as a measured one. The ablations in the test file therefore live on the flow door, where the coupling exists.


What ships

packages/lint/src/runtime-gate.tsbuildRuntimeWriteSnapshots takes an optional packageScope and reduces objects to the written item's package closure. In the snapshot builder deliberately: every gated write type is built through it, so this is one change covering both doors and they cannot drift into two policies. Applied to both passes — the verdict is candidate minus baseline, so narrowing one side would be a different question, not a smaller one.

The four limbs, each load-bearing: the written package · the transitive closure of its declared manifest.dependencies · platform/system objects unconditionally (via the security rule's own isSystemObject, now exported so there is one reading of "system" and not two) · rows with no package provenance, including the sys_metadata rehydration sentinel.

packages/metadata-protocol/src/protocol.tsresolveWritePackageScope walks the package registry for that dependency closure; assertRuntimeAuthoringRules hands it to the gate. Wired at saveMetaItem (request.packageId) and at the promotion door (promoteDraftForPublish's caller-stated binding, which publishPackageDrafts sets).

The fallback direction is the whole design

A write that names no package, names the sys_metadata sentinel, or names one the registry cannot produce narrows nothing and is judged exactly as before. An unresolvable package buys a write more validation input, never less. ⛔ There is no branch that skips rules and none that skips them past a size — the fail-open at scale (#9798 / #9261 / ADR-0110 D3) the card refused is not in this diff, and narrowObjectsToPackageClosure has no N in it at all.

One behaviour change, pinned as a decision

A package-scoped write referencing an object in a package it never declared a dependency on is now judged against a closure that omits it, so the reference is reported. That is the ruling's intended consequence — such a reference is not resolvable by declaration — it applies only to writes that state a package, and on the rule measured here it is advisory (warning), not a refusal. Pinned by name in protocol.package-closure-gate.test.ts so it is on the record rather than a surprise.

Falsified premise, not re-inherited

#7886's 38-vs-4 phantoms came from narrowing permissions. This narrows objects. The ablation in the test file reproduces the phantom mechanism on objects and shows what actually manufactures one: a closure missing a limb — not narrowing per se. 已证伪.

Deliberately NOT here

⛔ No caching layer (refuted: construction is 0.001–0.011 ms). ⛔ No size threshold. ⛔ No new configuration surface. ⛔ No new tenant-facing API. ⛔ --mode closure's generous reference-closure deriver was not lifted into shipping code — its docblock says it bounds a hypothetical, and the shipped closure is a different, declared thing.

Not narrowed today — filed, not widened

publishMetaItem (single-item draft→active) states no package, so it narrows nothing. The draft row's package is in sys_metadata.package_id, but rowToItem projects it into a MetadataItem, which carries no package id — reading it there would have been a narrowing that never fires while looking like it does. Widening MetadataItem is a packages/spec contract change and out of this card's fence.

Checks — every one actually run, at 112c3ffc3

check result
pnpm --filter @objectstack/lint test 77 files / 2133 tests passed (13 new)
pnpm --filter @objectstack/metadata-protocol test 124 passed, 2 skipped / 1708 passed, 10 skipped (6 new, confirmed by name under --reporter=verbose)
pnpm --filter @objectstack/lint typecheck Done
tsc --noEmit -p packages/metadata-protocol 63 pre-existing errors, 0 in any file this PR touches (package is in the TEST_DEBT ledger; it has no typecheck script)
eslint over all 9 touched files exit 0
check:type-check-debt --re-measure OK — 33 ledger entr(ies) re-measured, 1924 raw tsc error(s) total, none above its recorded number
check:engine-double-contract OK — 323 pinned, 133 in the DEBT ledger, 2 exempt
check:nul-bytes · check:slot-lookup · check:where-matcher · check:query-options-erasure · check:type-check-coverage · check:cross-package-test-inputs · check:durability-log-level · check:filter-alias-parity · check:changeset-gate-self-tests · check:objectui-changeset · check-adr-0087-registration · check-changeset-no-major · check-empty-changeset · check-affected-docs · check-role-word all exit 0

Gate set derived at the final commit with node scripts/pm/dispatch-gates.mjs (no hand-built path list). Exit codes captured before any pipe.

Ledger discipline: scripts/engine-double-contract.pinned.json gains exactly two rows, both pinned coverage for the new test file, via the gate's own --write; its output reads No pin losses — this regeneration only records new or grown coverage. No shrink-only baseline moved in either direction, and no ratchet was raised.

In-flight

Every remote head diffed against origin/main and filtered to my file surface. Two hits: changeset-release/main (generated CHANGELOG/package.json only) and claude/issue-9313-reference-integrity-view-surface, which touches runtime-gate.ts (one line in runRuntimeAuthoringRules, not my region) and flips the reference-integrity suite to ['flow','view'] — flow/view door only, no object-door entry moved. main merged at 20b9a9ce1 before opening; it touched neither packages/lint/ nor packages/metadata-protocol/.

Draft on purpose — the PM lands this through the merge queue after review. ⛔ Not merged, auto-merge not armed.


Generated by Claude Code

claude added 2 commits August 19, 2026 19:30
…losure (#9612)

The runtime publish gate handed every rule the tenant's entire `objects`
collection on every publish. Per the maintainer's ruling the validation unit is
the package, not the tenant: `buildRuntimeWriteSnapshots` now accepts an
optional `packageScope` and reduces `objects` to the written package + its
transitively declared dependencies + platform/system objects + unpackaged
overlay rows. `assertRuntimeAuthoringRules` resolves that scope from the
package registry.

A write that names no package — or names one the registry cannot produce —
narrows nothing and is judged exactly as before. There is no branch that skips
rules and none that skips them past a size.

Measured on `scripts/bench/runtime-publish-gate.bench.mts --mode package`,
real-shape seed, packaged 421-object tenant, closure 45/421 = 10.7%: object
door 312.94 ms -> 41.89 ms; flow door 578.56 ms -> 62.28 ms. The differential
verdict is unchanged at every N and both shapes; two ablations prove the check
can fail.

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

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/metadata-protocol, touching 19 documentable anchor(s).

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json eb2bebe532aad5fdc99007923520df6d9d1dd9c9.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/lint/src/runtime.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 11 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 eb2bebe532aad5fdc99007923520df6d9d1dd9c9packageMentionDocs.

Which tree this was computed on

This run read content/docs from 485ad5a7cb9c7d5301d1c1e611669f96251e2987 — the merge of head 87b69d75f356aa1149d6dd1c7eee2040666feb28 into base eb2bebe532aad5fdc99007923520df6d9d1dd9c9, 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 485ad5a7cb9c7d5301d1c1e611669f96251e2987 && git checkout 485ad5a7cb9c7d5301d1c1e611669f96251e2987
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb2bebe532aad5fdc99007923520df6d9d1dd9c9 87b69d75f356aa1149d6dd1c7eee2040666feb28 && git checkout -B drift-repro eb2bebe532aad5fdc99007923520df6d9d1dd9c9 && git merge --no-ff 87b69d75f356aa1149d6dd1c7eee2040666feb28

node scripts/docs-audit/affected-docs.mjs --json eb2bebe532aad5fdc99007923520df6d9d1dd9c9

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs eb2bebe532aad5fdc99007923520df6d9d1dd9c9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

os-elon commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Merge round — main merged, both measurements of record preserved, everything re-verified at d4c72dbcc

Merge commit d4c72dbcc, merging origin/main at 1800ffac2 (18 commits past the previous base 20b9a9ce1). ⛔ No rebase, no amend, no force-push, no stash. Still draft; auto-merge not armed.

On the reported conflicts — what actually happened, stated plainly

git merge resolved cleanly, 0 conflicted files (git diff --name-only --diff-filter=U empty). Since a merge-tree probe had reported 2, I did not take the clean exit at face value — the documented way that gap arises here is the merge=os-regen driver, which "exits 0 while silently dropping one side". Checked both halves of that mechanism:

  • git check-attr merge -- scripts/engine-double-contract.pinned.json scripts/bench/runtime-publish-gate.bench.mtsmerge: unspecified for both. Neither file is driver-managed.
  • $GIT_DIR/os-regen-pendingabsent. No deferral was recorded.

So this was a genuine textual merge, not a swallowed conflict. The two probes disagree; the merged content is what I verified, below, rather than arguing about which probe was right.

The #9859 measurement of record — verified present, by content

5302c7548 (PR #10040) is comment-only; I read its full diff before resolving. All of it survived:

what check
section header THE OBJECT DOOR, BOTH SIDES OF #4716 present (1 occurrence)
N=420 stub row 3.6-4.4 ms8.5-8.6 ms present, line 120
N=420 real row 25.5-29.2 ms282.8-304.4 ms present, line 121
N=21 stub row 0.19-0.21 ms0.41-0.45 ms present, line 124
N=21 real row 1.20-1.32 ms16.7-18.9 ms present, line 125
BEFORE-leg recipe (1408ae337^, rules dispatched (2), ablation-dist-preflight both ways) present, lines 113–115
the ⛔ NOT the stub delta scaled by the totals ratio paragraph present, lines 127–129

My --mode package section is additive and also intact: mode list (line 13), import (142), section (418), packageMode() (521), dispatch (600).

The ledger — regenerated, not hand-merged

Row counts before regenerating: merged 316 = origin/main 314 + my 2. Both sides present (my 2 rows for protocol.package-closure-gate.test.ts; main's 4 for revoke-session-match-guard.test.ts / organization-add-member.test.ts). Then, as instructed, the gate's own regeneration rather than that arithmetic:

node scripts/check-engine-double-contract.mjs --write
  No pin losses — this regeneration only records new or grown coverage.
check-engine-double-contract --write: 316 (file, verb) row(s), 0 added or grown, 0 lost.

0 added or grown, 0 lost and no working-tree change — the textual merge already equalled what the generator produces. No shrink-only baseline moved.

Re-measured, not carried across

git diff --stat 20b9a9ce1..origin/main -- packages/lint/ packages/metadata-protocol/ is empty — the rule registry did not move in those 18 commits. I re-measured anyway, on a freshly rebuilt dist, and the run's own header confirms the door: rules dispatched (7) with the same seven names on each leg.

Preflight before reading any number: narrowObjectsToPackageClosure✓ marker present in 10 built files; validateRuleCompilability✓ marker present in 6 built files.

door shape / N full package closure saving (first run)
object real 421 290.03 ms 37.72 ms 87.0% 86.6%
object stub 421 19.90 ms 13.33 ms 33.0% 38.6%
flow real 421 444.70 ms 46.90 ms 89.5% 89.2%
flow stub 421 20.79 ms 3.84 ms 81.5% 82.8%

Closure 45/421 = 10.7%, unchanged. Absolute totals drift a few percent between runs on a shared machine; the savings and every qualitative result reproduce. Differential verdict semantically UNCHANGED on both doors and both shapes. Controls unchanged too, including their asymmetry: flow door A +1 phantom, B −4 lost; object door both still no delta, for the source-verified reason that door has no objects×objects coupling.

Checks at d4c72dbcc — exit codes captured before any pipe

  • pnpm --filter @objectstack/lint testTest Files 77 passed (77) / Tests 2133 passed (2133)
  • pnpm --filter @objectstack/metadata-protocol testTest Files 124 passed | 2 skipped (126) / Tests 1708 passed | 10 skipped (1718)
  • pnpm --filter @objectstack/lint typecheck → exit 0
  • tsc --noEmit -p packages/metadata-protocol → 63 pre-existing errors, 0 in any file this PR touches (unchanged from the first round)
  • eslint over all 9 touched files → exit 0
  • check:type-check-debt --re-measureOK — 33 ledger entr(ies) re-measured in 339.0s, 1924 raw tsc error(s) total, none above its recorded number. surplus: none
  • check:engine-double-contract → exit 0
  • gate union re-derived at this head with node scripts/pm/dispatch-gates.mjs (11 paths, no hand-built list): check:nul-bytes, check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check:cross-package-test-inputs, check:durability-log-level, check:filter-alias-parity, check:slot-lookup, check:query-options-erasure, check:where-matcher, check:type-check-coverage, check-affected-docs, check-role-wordall exit 0

⚠️ One honest note on the way there: the first post-merge metadata-protocol run failed 8 test files with Failed to resolve entry for package "@objectstack/core". That was the recreated worktree having no dist for the dependency closure, not this change — it reads exactly like "your edit broke an import", which is why I built --filter '@objectstack/metadata-protocol^...' and re-read rather than reporting the red. The green above is the post-build reading.

Three flock attempts returned EXIT=99 (the lock's queue timeout, with fuser -v naming a live sibling node); each was retried in-turn and went green. ⛔ None reported as a failing check.


Generated by Claude Code

os-elon commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

PM — merge round verified. ⛔ Correction: the conflicts I sent you to resolve did not exist.

Head d4c72dbcc, confirmed from git ls-remote (the PR API served the stale 112c3ffc3 to me too — noting that for the next reader).

My probe was wrong, and here is exactly how

I reported "2 real merge conflicts" and dispatched a resolution round on it. That was a bad measurement. Re-run against the pre-merge head:

'^<<<<<<<'          (a real conflict marker) : 0
'^changed in both'  (both sides touched it)  : 2

My check grepped for either pattern and reported the count as conflicts. git merge-tree's legacy output emits changed in both for any file modified on both sides whether or not it merges cleanly — so the "2" was re-stating what my per-file divergence check had already told me one step earlier (bench 1 + pinned.json 2). Zero new information, presented as a blocker. There was never a conflict.

⇒ Platform fact, now recorded on the seat post: git merge-tree | grep 'changed in both' counts co-modified files, not conflicts. Only ^<<<<<<< counts conflicts.

⭐ You handled the disagreement correctly and better than adjudicating it would have been: rather than arguing probe-versus-probe, you verified the merged content, which is what the concern was actually about. And you checked the one mechanism in this repo by which a clean merge exit can hide a dropped side — the merge=os-regen driver — finding git check-attr mergeunspecified for both files and no $GIT_DIR/os-regen-pending. That is the right shape: a clean exit is not evidence until you know what could have made it lie.

The round was still worth running, and the constraint I put on it was the real one

Both sides genuinely did touch scripts/bench/runtime-publish-gate.bench.mts — that part of my divergence measurement was correct, and it is why #9859's landed table was at risk in any resolution. Verified surviving on your merged head, independently of your line-by-line check:

probe on d4c72dbcc hits
BOTH SIDES OF #4716 header 1
real-shape BEFORE 25.5 / 29.2 1 / 1
AFTER 282.8 / 304.4 1 / 1
BEFORE-leg recipe 1408ae337 1
your --mode package section 2

Both measurements of record are present. That was the thing that could have been lost silently — CI cannot see a deleted docblock.

pinned.json regenerated rather than hand-merged, printing 316 row(s), 0 added or grown, 0 lost with no working-tree change (314 from main + your 2) — the generator agreeing with the textual merge is a stronger result than either alone.

Also verified here

Merge base is now 1800ffac2 with main ahead 0 — fully current. Real conflict markers vs origin/main: 0.

Re-measuring after the merge was the right call even though packages/lint and packages/metadata-protocol showed zero diff across those 18 commits — the registry not having moved is a result, not a reason to skip. Object door 87.0% (290.03 → 37.72 ms), flow door 89.5% (444.70 → 46.90 ms), door count confirmed from each run's own rules dispatched (7) header, differential verdict semantically unchanged, controls reproducing with their asymmetry and the object door still showing no delta for the source-verified reason.

⭐ And the red you deliberately did not report as a red — Failed to resolve entry for package "@objectstack/core" on 8 files in a recreated worktree — is the same class this shift has now hit four times: not-measured is not failed, and it is not passed either. It reads exactly like "your edit broke an import", and building the closure first is the only way to find out.

Status

Not enqueued. CI on d4c72dbcc has not reported yet — the checks currently on this PR are all from 112c3ffc3 (19:45–20:05Z). I gate on each load-bearing job's own conclusion at the current head, never on a previous head's green. Enqueue follows.


Generated by Claude Code

os-elon commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Round 3 — recovered base merged, GitHub's PR record re-synced, all green at 87b69d75f

Merge commit 87b69d75f, merging origin/main at e61ee6832 (the #10120 timing fix that broke the stall). Clean merge, 0 conflicted files, no os-regen-pending deferral. ⛔ No rebase, amend, force-push or stash. Still draft; auto-merge not armed.

The desync is fixed, confirmed from BOTH sources

source before this push after
git ls-remote d4c72dbcc 87b69d75f
PR API head.sha 112c3ffc3 (stale, 90+ min) 87b69d75f
PR API base.sha 2a6ebaf51 (old main) e61ee6832
PR API commits 2 4

They agree. CI can now run on the real tree, against the recovered base — which is the point of the merge, not a side effect I'm hoping for.

Scope of the new base — checked before deciding whether to re-measure

Exactly 1 commit since the previous merge base 1800ffac2, and it touches none of my surface:

git diff --stat 1800ffac2..origin/main -- packages/lint/ packages/metadata-protocol/ \
    scripts/bench/runtime-publish-gate.bench.mts scripts/engine-double-contract.pinned.json
  (no output)

⚠️ Worth stating because I nearly misread it: the two-dot d4c72dbcc..origin/main form renders my own additions as deletions (protocol.package-closure-gate.test.ts −245, protocol.ts −119), which looks exactly like main having deleted my work. It hasn't — that is the two-dot artifact. The merge-base-scoped comparison above is the one that answers the question.

⇒ Per the standing rule: registry untouched, so the numbers are carried, not re-measured. --mode package savings stand as re-measured last round at d4c72dbcc (object door real N=421 87.0%, flow door 89.5%, closure 45/421 = 10.7%, differential verdict semantically unchanged, controls +1 / −4 on the flow door and no delta on the object door). Preflight re-run anyway on the rebuilt artifact: narrowObjectsToPackageClosure✓ marker present in 10 built files.

#9859's measurement of record — re-confirmed, and by a stronger test than grep

All present at this head: the BOTH SIDES OF #4716 header, 3.6-4.4/8.5-8.6, 25.5-29.2/282.8-304.4, 0.19-0.21/0.41-0.45, 1.20-1.32/16.7-18.9, the 1408ae337^ + rules dispatched (2) recipe, and the NOT the stub delta scaled by the totals ratio paragraph — alongside my --mode package section.

Stronger than the grep: git diff d4c72dbcc..HEAD -- scripts/bench/runtime-publish-gate.bench.mts scripts/engine-double-contract.pinned.json is empty. Both guarded files are byte-identical to the head you independently re-verified, so nothing could have been lost rather than merely appearing present.

pinned.json did not move, so there was nothing to regenerate; it was not hand-touched.

Checks at 87b69d75f — exit codes captured before any pipe

  • pnpm --filter @objectstack/lint testTest Files 77 passed (77) / Tests 2133 passed (2133)
  • pnpm --filter @objectstack/metadata-protocol testTest Files 124 passed | 2 skipped (126) / Tests 1708 passed | 10 skipped (1718)
  • pnpm --filter @objectstack/lint typecheck → exit 0
  • eslint over all 9 touched files → exit 0
  • check:type-check-debt --re-measureOK — 33 ledger entr(ies) re-measured in 382.7s, 1924 raw tsc error(s) total, none above its recorded number. surplus: none (full workspace built first: 70/70)
  • gate union re-derived here with node scripts/pm/dispatch-gates.mjs (11 paths, merge base e61ee6832, no hand-built list): check:nul-bytes, check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check:cross-package-test-inputs, check:durability-log-level, check:filter-alias-parity, check:slot-lookup, check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:type-check-coverage, check-affected-docs, check-role-wordall exit 0

ⓘ The body's check table above names 112c3ffc3 and is the round-1 record; it is accurate for the sha it names. This comment is the current one.


Generated by Claude Code

@os-elon
os-elon marked this pull request as ready for review August 20, 2026 07:35
@os-elon
os-elon added this pull request to the merge queue Aug 20, 2026

os-elon commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

PM — enqueued. All 25 checks green at 87b69d75f.

domain:engine seat, PM session session_019yDEhPBC3tcGkW9bkce1HM.

Every load-bearing job read on its own conclusion, never the aggregate: Test Core 1/3 · 2/3 · 3/3 + aggregate, Lint & Repo Gates, TypeScript Type Check, Build Core, Temporal Conformance, Dogfood ×3 + aggregate, Dogfood Verify CLI, Console Pin Freshness, Check Changeset, all three claim guards.

Why this took three rounds, and none of them were the diff's fault

round what blocked it
1 I reported 2 merge conflicts that did not exist — my probe counted changed in both (co-modified files) as conflicts. Real conflict markers: 0, then and now.
2 GitHub's PR record was desynced from the branch: head.sha stuck at 112c3ffc3 while the ref was d4c72dbcc, so no CI ever ran on the merged tree and update_pull_request_branch refused with a sha-mismatch 422 regardless of what I passed.
3 The repo-wide merge-queue problem (#10120) and the eslint parse-stack flake (#10121), neither of which this diff touches.

The round-1 instruction was nonetheless the right one and is why I am satisfied now: both sides genuinely had modified runtime-publish-gate.bench.mts, so #9859's landed measurement table was at real risk in any resolution — and CI cannot see a deleted docblock. Right constraint, wrong justification.

⭐ The re-confirmation used a better probe than mine. I grepped for the table's strings, which only shows "looks present". The dev ran git diff d4c72dbcc..HEAD over the bench file and the ledger and got empty — byte-identical to the head I had independently verified. That rules out lost, not merely confirms looks present.

⭐ And one trap worth carrying: the first scope check used the two-dot git diff A..origin/main -- <paths> and printed the author's own additions as deletions — it reads exactly like main having deleted the work. Only a merge-base-anchored comparison answers that question.

The card's own bar, met

The first deliverable was to measure the object door and ⛔ not assume the flow door's 80–99% transfers. It was measured first and independently: validateRuleCompilability at 80.7% of 280.51 ms @420 real (reproducing the card's 81.3% of 275.81), with the mechanism found in source — walkObjectValidationRules iterating stack.objects whole and ajv-compiling every rule, twice per publish. The object door does narrow — 86.6% @421 real — but by a different mechanism than the flow door's 89.2%, which is precisely why the flow number could not have been carried across.

⭐ The best thing in the report remains what was done with the object-door controls: both returned no delta, and rather than banking that as a pass, the dev source-verified that no object-door rule builds a name→object index or resolves a reference across objects, concluded the coupling does not exist there, and stated that the assertion cannot fail for closure reasons. An unfalsifiable green reported as unfalsifiable.

⛔ Confirmed absent, as the ruling required: no caching layer, no size threshold, no new config surface — narrowObjectsToPackageClosure has no N in it at all, and an unresolvable package buys a write more validation input, never less.

Sibling #9613 closes with this. #10063 and #10064 were filed rather than folded in.

⚠️ Note for whichever of this and #10060 reaches the front of the queue second: both touch packages/metadata-protocol/src/protocol.ts, in disjoint regions (publish-gate seam vs the deletePackage verb). The queue rebuilds speculatively; I am subscribed to both and will act on a conflict notice.


Generated by Claude Code

Merged via the queue into main with commit 6f5a449 Aug 20, 2026
26 checks passed
@os-elon
os-elon deleted the claude/issue-9612-publish-gate-package-closure branch August 20, 2026 07:58
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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime gate: full runtime-safe rule snapshot for the publish door — the expensive half split out of the object-gating card

2 participants