Skip to content

feat(lint): the five gating object rules cross the runtime publish gate (#4716) - #9825

Merged
os-elon merged 5 commits into
mainfrom
claude/issue-4716-object-write-gate-p2
Aug 19, 2026
Merged

feat(lint): the five gating object rules cross the runtime publish gate (#4716)#9825
os-elon merged 5 commits into
mainfrom
claude/issue-4716-object-write-gate-p2

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#4716

What this is

The 2026-08-18 maintainer adjudication's narrowed scope (issue comment 5328603673, converted to an executable scope in 5328674256): the five gating rules of the former RUNTIME_OBJECT_WRITES_P2 group cross onto the object write door — validateFunctionalCompleteness, validateManagedApiMethods, lintAutonumberFormats, validateRuleCompilability, validateRuleSchemaFormats gain surfaces: CLI_AND_RUNTIME + runtimeTypes: ['object'].

An active-state object save through saveMetaItem (Studio's field editor, REST /meta item CRUD, an MCP/AI author) is now refused with the existing 422 invalid_metadata envelope when it carries a defect these five rules judge. All five already gated os validate / os build / os lint; the runtime door — the only door a tenant overlay row has — ran none of them.

Deliberately NOT in this PR (each fenced by the adjudication): the six advisory-tier object rules (their surfaceReason is rewritten to RUNTIME_OBJECT_ADVISORY_VOLUME, and both the lint-layer and protocol-layer fence tests pin them out by name); RUNTIME_NEEDS_FULL_SNAPSHOT (split to #9612); any advisory→gating severity upgrade; validateSecurityPosture consolidation; TYPE_TO_STACK_KEY widening; #9600.

The false-positive budget — exempted, with its caveat in as many words

The maintainer exempted the false-positive budget on the 2026-08-18 measurement (0 refusals / 75 real object declarations across two authoring lineages, 0 phantoms after the instrument fix, non-vacuity proven by 6 synthetic violations refusing correctly). Route A — replaying real tenant sys_metadata overlay rows — was ruled structurally unavailable, not declined: objectstack cloud has not launched, so no tenant exists whose stored overlay rows could be exported. Maintainer, verbatim: 「objectstack cloud 还没有正式上线,简化开发。」

The lower-bound caveat, which is part of the exemption: every measured population is authored config-file metadata — the cleanest possible input — so 0/75 is a lower bound on the refusal rate, not an estimate of it. Post-launch follow-up expectation (an expectation, not a gate): when the first real tenants exist, a replay of stored overlay rows is run as an after-the-fact audit of this exemption.

The six refusal controls from that measurement are pinned permanently in runtime-gate.object-writes.test.ts, so the evidence the exemption rests on cannot rot silently.

The lazy-deps tripwire fired — and was re-pinned deliberately

runtime-lazy-deps.test.ts's object leg (PR #9295) had never been exercised, because firing it requires exactly this widening. Fired on this branch, mechanically verified:

  • dist preflight: widening marker present in all 4 built files (scripts/ablation-dist-preflight.mjs @objectstack/lint 'Advisory-tier object rule');
  • the pre-[P2] 运行时授权门扩到 object 写入 + 全量 runtime-safe 规则快照(#4463 P1 之后) #4716 test against the widened dist: RED, 3 of 6 legs (spawned CJS, spawned ESM, in-process object leg), each failure naming ajv loaded while gating the schema-carrying object write — the tripwire works, and it fired for the reason it was built;
  • the re-pinned three-tier contract against the same dist: GREEN 6/6, restore byte-identical (git status clean).

The re-pin is the "stop and think" the red demanded: source parsers (typescript/sucrase) still load never; ajv still loads never at import and never on a schema-free object write (the hot path — an ordinary Studio field edit pays for no compiler); ajv is required to load, lazily, when an object write carries a json_schema validation — and the gate must refuse the uncompilable one. Tier 3 is what keeps tier 2's clean reads falsifiable.

Cost delta — measured, not worsened silently

Measured on the built gate (dist/runtime.cjs, medians of 30 iterations after warmup, same machine and process shape for both legs; BEFORE = origin/main registry rebuilt and preflighted absent, AFTER = this branch, preflighted present):

writeN stored objectsBEFOREAFTERdelta
object, schema-free (clean)210.21 ms (2 rules)0.52 ms (7 rules)+0.31 ms
object, schema-free (clean)4203.65 ms (2 rules)6.90 ms (7 rules)+3.25 ms
flow publish212.00 ms2.08 msnone (within noise)
flow publish42020.11 ms20.20 msnone (within noise)
object carrying a json_schema validation210.38 mscold 61.8 ms / warm 7.2 msthe adjudicated ajv price

The pre-existing linear cost of the shipped gate (33.4 ms @ 21 → 506 ms @ 420 per flow publish, measured at 5309093362) is its own finding, pre-existing and untouched here — the flow rows above show this PR adds nothing to it. The ajv cold load is once per process; warm is per schema-carrying publish only.

The one consumer red, and its repair

The widening turned exactly one downstream test red: packages/runtime/src/meta-field-overlay-lock.test.ts's control "object is still refused, exactly as before" — its packaged-object fixture carried an optionless select (incidental), so validateFunctionalCompleteness's 422 now answered before the overlay lock's 403. The fixture's own #8310 comment records the established resolution for precisely this interaction (the 422 lint door speaks first; keep control bodies clean so the lock's sentence stays under test): the select now declares options, and the comment carries the #4716 note. No production ordering was changed.

Evidence

  • At the final head 3b7f599 (post-merge of today's origin/main, full packages closure rebuilt): @objectstack/lint 75 files / 2099 tests green and tsc --noEmit clean; @objectstack/metadata-protocol 123 files / 1702 tests green (no typecheck script — its type gate is the tsup DTS build, green in the closure); @objectstack/runtime 174 files / 2598 tests green.
  • Consumer sweep, downstream direction (direct dependents of the two changed packages), run at 7218142 (same tree minus a merge that did not touch them beyond what CI re-verifies): @objectstack/rest 129 files, @objectstack/cli 134 files, @objectstack/objectql 219 files / 3878 tests, @objectstack/platform-objects 25 files, @objectstack/plugin-approvals 24 files — all green after the one fixture repair above.
  • Reverse verification of the new pins (ablate the widening in src+dist, preflight absent): runtime-gate.object-writes.test.ts10/11 red (only the direction-neutral D4 differential case stays green); the protocol block's two refusal legs red; every other protocol case is refusal-free in both states by design. Restored, rebuilt, preflighted present, all green, tree byte-identical.
  • Premises re-derived post-merge (registry read from built dist, never line numbers): 41 rules; runtime-publish 11 → 16 (exactly the five crossed); cli-only 30 → 25 (13 full-snapshot + 2 heavy-parse + 6 advisory-volume + 4 inline); gated types unchanged (8, incl. object); object door = exactly 7 rules in registry order; flow door untouched; TYPE_TO_STACK_KEY 11 mappings, no report/dataset/app.
  • Gate union re-derived at the final head 3b7f599 with node scripts/pm/dispatch-gates.mjs (no hand-fed paths — the script derives the 6-file change set from the merge base), all green: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:durability-log-level, check:objectui-changeset, check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:where-matcher, check:nul-bytes, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-affected-docs, and check:type-check-debt --re-measure on the BUILT closure ("33 ledger entries re-measured, 1926 raw tsc errors total, none above its recorded number; surplus: none").

Generated by Claude Code

…te (#4716)
The 2026-08-18 adjudication's narrowed scope: validateFunctionalCompleteness,
validateManagedApiMethods, lintAutonumberFormats, validateRuleCompilability and
validateRuleSchemaFormats gain runtimeTypes: ['object']; the six advisory-tier
object rules stay behind (advisory-volume decision, its own card). The
false-positive budget was exempted on 0 refusals / 75 real object declarations
(authored config-file metadata — a lower bound), with a post-launch overlay-row
replay as the standing audit.
runtime-lazy-deps.test.ts re-pins the boot-path contract as three tiers after
its object leg (PR #9295) fired red on this widening as designed: parsers
never load; ajv never loads at import or on schema-free writes; ajv is
REQUIRED to load, lazily, when an object write carries a json_schema
validation — and the gate must refuse the uncompilable one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qYPmkKEsfbWY1yVg83p8F
…Zod-required message on the json_schema fixture (#4716)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qYPmkKEsfbWY1yVg83p8F
… the 403 stays the sentence under test (#4716)
The five gating object rules now answer at the 422 lint door, which speaks
before the overlay lock (the established #8310 ordering, recorded in this
fixture's own comment). The control body carried an optionless select —
incidental, and now refused by validateFunctionalCompleteness as
field/choice-without-options — so the NOT_OVERRIDABLE control was refused for
the wrong reason. Authoring the options restores the fixture to what it pins.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 5 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/validating-metadata.mdx(via AUTHORING_RULES (symbol), runtimeTypes (symbol))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via AUTHORING_RULES (symbol), runtimeTypes (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 4 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 9ff11921a26f44a62ca834651adaa54105468536packageMentionDocs.

Which tree this was computed on

This run read content/docs from c0bdc3285343dcaf97f352722b32fc07990febef — the merge of head 3b7f59907f108df395de8ad064dcbd3f53fc9bc3 into base 9ff11921a26f44a62ca834651adaa54105468536, 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 c0bdc3285343dcaf97f352722b32fc07990febef && git checkout c0bdc3285343dcaf97f352722b32fc07990febef
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9ff11921a26f44a62ca834651adaa54105468536 3b7f59907f108df395de8ad064dcbd3f53fc9bc3 && git checkout -B drift-repro 9ff11921a26f44a62ca834651adaa54105468536 && git merge --no-ff 3b7f59907f108df395de8ad064dcbd3f53fc9bc3
node scripts/docs-audit/affected-docs.mjs --json 9ff11921a26f44a62ca834651adaa54105468536

⚠️ 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 9ff11921a26f44a62ca834651adaa54105468536 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-elonClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT, ready flipped, enqueued

Reviewed against GitHub, not against the report. Squash-merge auto-merge enabled.

Per-file divergence — clean, with a control that fires

The check that matters for this branch is per file, never per package: packages/lint and packages/runtime both move constantly, so a package-level reading gives false alarms and false clearances alike.

Merge base 593c4bf3f (#9820). Since then origin/main advanced 7 commits touching 25 files:

proberesult
commits on origin/main after merge base touching any of this PR's 6 files0
intersection of main's 25 touched files with this PR's 6empty
positive control — same command shape on a file main did move (.changeset/anonymous-deny-401-code-key.md)1 — the probe can return non-zero

The zero is a measurement, not a silence: the identical command returns 1 for a file main touched, so a collision on authoring-rules.ts would have been visible.

CI

All 25 check runs completed on head 3b7f599: 23 success, 2 skipped, 0 failures. Lint & Repo Gates ✅, TypeScript Type Check ✅, Build Core ✅, Test Core ×3 + aggregate ✅, Dogfood Regression Gate ×3 + aggregate ✅, Dogfood Verify CLI ✅, Temporal Conformance ✅.

What I checked beyond green

  • The exemption is load-bearing and is stated as one. The FP budget was exempted by maintainer adjudication 5328603673 on grounds that Route A is structurally unavailable (no launched tenant ⇒ no stored overlay rows to replay), not declined. The PR body carries the lower-bound caveat in as many words — 0/75 measured on authored config-file metadata is a floor on the refusal rate, not an estimate of it — and pins the six refusal controls so the evidence cannot rot silently. That is the right shape for an exemption: the weakness is in the record, not papered over.
  • The lazy-deps tripwire fired and was re-pinned deliberately, not silenced. Ablated against the widened dist the pre-[P2] 运行时授权门扩到 object 写入 + 全量 runtime-safe 规则快照(#4463 P1 之后) #4716 test goes red on 3 of 6 legs, each naming ajv loaded while gating a schema-carrying object write; the re-pinned three-tier contract is green 6/6 with a byte-identical restore. Tier 3 (ajv must load for a json_schema write) is what keeps tier 2's clean reads falsifiable — a tripwire re-pinned without it would have been a silencing.
  • The one consumer red was repaired at the fixture, not at the ordering.meta-field-overlay-lock.test.ts's control fixture carried an incidental optionless select, so the new 422 answered before the lock's 403. The fixture's own [3 of #7891] Flip the registration: runtimeTypes gains object + permission/book — whole rule families cross the publish door #8310 comment already records this exact interaction's resolution. No production ordering changed.

One finding, filed rather than blocked: this PR falsifies three rows of a shipped doc

The drift check flagged content/docs/deployment/validating-metadata.mdx. I verified it — the flag is correct and concrete. That page's four-door table has a runtime publish column, and five rules crossing that door means three rows currently read where they will read ✓ᵒ:

linerowcovers
:392Zod-valid but functionally inert declarations (ADR-0078, #7521)validateFunctionalCompleteness, validateManagedApiMethods
:411Declared enforcement that cannot run (#4698, #4983, #4762, #5178)validateRuleCompilability, validateRuleSchemaFormats
:413Autonumber {field} interpolationlintAutonumberFormats

Control on that reading: the page already uses in two other rows (✓ᵈᵛᵒᵖᶠ, ✓ˢᵖᵉᵇᵒ), so the in these three is a claim about the object door, not an artifact of a page that doesn't track object.

:411 is not a cell edit — it bundles four rule ids across the wall: #4762/#5178 cross here, #4698/#4983 do not. Under the page's own stated convention ("that rule's own runtimeTypes in AUTHORING_RULES is the authority") the row has to be split so each side reads truthfully. That is the same one-rule-id-one-side-of-the-wall problem the page itself works through in the vocabulary-freeze paragraph.

Why this is not a hold on this PR. The repo's own check-affected-docs gate is green and the drift check is advisory by construction. More to the point, validating-metadata.mdx is touched by every rule-crossing PR — it is the conflict magnet the releases-page guardrail exists to describe — and the :411 split is an editorial judgement about how a table documents a wall. That deserves its own diff and its own review, not a rider appended to a 697-line code PR under queue pressure. Filed as its own card and dispatched in this same round, so main does not carry the wrong table for long.

The crossing-history clause (#4463 … #7220 … #7529 … #8307 → #8310) needs #4716 appended in the same edit.


Generated by Claude Code

@os-elonClaude

Copy link
Copy Markdown
CollaboratorAuthor

The docs finding above is filed as #9836, carrying the three rows, the :411 split problem, and the non-vacuity control.

One correction to what I wrote: #9836 is Blocked-by: #4716 and cannot be dispatched before this PR merges — the card's job is to describe the object door's landed state, so its verification has to re-derive the registry on the merged ref. It is queued to dispatch the moment this merges, not in parallel with it.


Generated by Claude Code

Merged via the queue into main with commit 1408ae3Aug 19, 2026
26 checks passed
@os-elon
os-elon deleted the claude/issue-4716-object-write-gate-p2 branch August 19, 2026 04:05
os-elon pushed a commit that referenced this pull request Aug 19, 2026
…time door after #4716 (#9836)
Three rows of the "one gate, four doors" table read `—` in the `runtime
publish` column for rules that now run there. #4716 (PR #9825, squash
1408ae3) crossed five gating object rules onto that door, taking it from
2 rules to 7.
Corrected against the built registry, not the source text:
:392 functional completeness + managed API methods — -> ✓ᵒ
:413 autonumber `{field}` interpolation — -> ✓ᵒ
The "declared enforcement that cannot run" row is SPLIT rather than
edited. It bundled four rule ids that now sit on opposite sides of the
wall: `validateRuleCompilability` (#4762) and `validateRuleSchemaFormats`
(#5178) carry `runtimeTypes: ['object']`, while
`validateSharingRuleEnforceability` (#4698) and
`validateRlsPredicateEnforceability` (#4983) are `CLI_ONLY`. `✓ᵒ` on the
merged row would claim the door judges an RLS predicate on an object
write; `—` keeps the current falsehood. The split is drawn by which
collection carries the declaration each rule reads, and a new paragraph
states that rule so the next crossing does not re-derive it — following
the precedent the page already sets for the security vocabulary freeze.
Also: #4716 appended to the crossing-history clause, and the `—`-reasons
paragraph gains the reason class #4716 created. All four existing reasons
still hold, but the advisory-tier object rules moved: their old
`RUNTIME_OBJECT_WRITES_P2` reason is retired (0 occurrences), replaced by
`RUNTIME_OBJECT_ADVISORY_VOLUME` on six rules whose registry text says
crossing them is "explicitly NOT a bare `runtimeTypes` edit" — the
opposite of what the paragraph's third reason would have told a reader.
No change to `AUTHORING_RULES`, no rule's `runtimeTypes`, nothing under
`content/docs/releases/`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P2] 运行时授权门扩到 object 写入 + 全量 runtime-safe 规则快照(#4463 P1 之后)

2 participants

@os-elon@claude