Uh oh!
There was an error while loading. Please reload this page.
feat(lint,metadata-protocol): judge a package write against its own closure - #10058
feat(lint,metadata-protocol): judge a package write against its own closure#10058os-elon wants to merge 2 commits into
Conversation
…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
…lish-gate-package-closure
📓 Docs Drift CheckThis PR changes 2 package(s): 17 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 — 11 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 dcee04c0f1e2e8f239b615aa75434529122a0a09 && git checkout dcee04c0f1e2e8f239b615aa75434529122a0a09
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2a6ebaf519535a3332b50ddd90d081de8e135a4f 112c3ffc3106d144f12fe5917a0c44bd4761adac && git checkout -B drift-repro 2a6ebaf519535a3332b50ddd90d081de8e135a4f && git merge --no-ff 112c3ffc3106d144f12fe5917a0c44bd4761adac
node scripts/docs-audit/affected-docs.mjs --json 2a6ebaf519535a3332b50ddd90d081de8e135a4f
|
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:
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:1. The object door's driver, reproduced (
--mode per-rule --type object)rules dispatched (7): validateFunctionalCompleteness, validateManagedApiMethods, validatePresetComparands, lintAutonumberFormats, validateSecurityPosture, validateRuleCompilability, validateRuleSchemaFormatsvalidateRuleCompilabilityvalidatePresetComparandsThe card's 81.3% of 275.81 ms reproduces at 80.7% of 280.51 ms.
validatePresetComparandsis 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:
validateRuleCompilabilityreachesobjectsthroughwalkObjectValidationRules(stack), which iteratesstack.objectswhole and hands everyjson_schema/formatrule 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 shippednarrowObjectsToPackageClosureon 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.
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.
3. Differential verdict — and one honest caveat
Semantically UNCHANGED at every N, both shapes, both doors.
pathreports+1 / -1on 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, andwherealready carries the object name. Both readings are printed by the bench rather than the inconvenient one being hidden — the index really is wire-visible inissues[].path, and it is an index into a snapshot array no caller has ever seen.4. Positive controls — and where they are silent
Control A reproduces the #7886 mechanism on
objects:flow-trigger-unknown-objecton 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.objects—validateSecurityPosturecrosses objects × permissions and objects × books, never objects × objects;lintAutonumberFormats's onlyMapis 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.ts—buildRuntimeWriteSnapshotstakes an optionalpackageScopeand reducesobjectsto 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 ownisSystemObject, now exported so there is one reading of "system" and not two) · rows with no package provenance, including thesys_metadatarehydration sentinel.packages/metadata-protocol/src/protocol.ts—resolveWritePackageScopewalks the package registry for that dependency closure;assertRuntimeAuthoringRuleshands it to the gate. Wired atsaveMetaItem(request.packageId) and at the promotion door (promoteDraftForPublish's caller-stated binding, whichpublishPackageDraftssets).The fallback direction is the whole design
A write that names no package, names the
sys_metadatasentinel, 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, andnarrowObjectsToPackageClosurehas noNin 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 inprotocol.package-closure-gate.test.tsso 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 narrowsobjects. The ablation in the test file reproduces the phantom mechanism onobjectsand 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 insys_metadata.package_id, butrowToItemprojects it into aMetadataItem, which carries no package id — reading it there would have been a narrowing that never fires while looking like it does. WideningMetadataItemis apackages/speccontract change and out of this card's fence.Checks — every one actually run, at
112c3ffc3pnpm --filter @objectstack/lint testpnpm --filter @objectstack/metadata-protocol test--reporter=verbose)pnpm --filter @objectstack/lint typecheckDonetsc --noEmit -p packages/metadata-protocoltypecheckscript)eslintover all 9 touched filescheck:type-check-debt --re-measureOK — 33 ledger entr(ies) re-measured, 1924 raw tsc error(s) total, none above its recorded numbercheck:engine-double-contractOK — 323 pinned, 133 in the DEBT ledger, 2 exemptcheck: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-wordGate 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.jsongains exactly two rows, both pinned coverage for the new test file, via the gate's own--write; its output readsNo 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/mainand filtered to my file surface. Two hits:changeset-release/main(generated CHANGELOG/package.json only) andclaude/issue-9313-reference-integrity-view-surface, which touchesruntime-gate.ts(one line inrunRuntimeAuthoringRules, not my region) and flips the reference-integrity suite to['flow','view']— flow/view door only, no object-door entry moved.mainmerged at20b9a9ce1before opening; it touched neitherpackages/lint/norpackages/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