Skip to content

fix(metadata-protocol): fold publish Phase 2's object-table lookup at the producer - #8867

Merged
hotlong merged 4 commits into
mainfrom
claude/issue-8820-ensure-object-storage-objects-limb
Aug 15, 2026
Merged

fix(metadata-protocol): fold publish Phase 2's object-table lookup at the producer#8867
hotlong merged 4 commits into
mainfrom
claude/issue-8820-ensure-object-storage-objects-limb

Conversation

@hotlong

@hotlonghotlong commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Fixes#8820

runPublishSideEffects (publish Phase 2) took two type parameters — a folded singularType and an unfolded requestType — and ensureObjectStorage was the sole consumer of the unfolded one. That is a spelling-tolerant lookup one layer below a boundary that already folds, the shape canonicalMetaType's header has rejected since #4432.

This passes args.singularType and removes requestType from the parameter list entirely, so no future consumer can reach for the unfolded value.

This is a refactor, not a fix — and the card's stated reason was wrong

ensureObjectStorage reads typeonly in its guard; it never reaches syncObjectSchema, which takes name alone. Before: 'objects' passes limb 2, calls syncObjectSchema(name). After: 'object' passes limb 1, calls syncObjectSchema(name). Byte-identical side effect, no public surface touched (the method is private, requestType was a private args field).

The card proposed deleting ensureObjectStorage's && type !== 'objects' limb on the premise that "both call sites stand behind the /meta fold". That premise is false, and an in-tree comment asserted it too. The helper's second call site sits insiderunPublishSideEffects, which has two callers:

  • publishMetaItem — folded at the /meta boundary.
  • publishPackageDraftsnot folded. It passes the draft row's stored type, and listDrafts applies no fold (sys-metadata-repository.ts maps type: row.type straight through).

Counting the helper's own call sites is not the reachability question.

Why the conclusion nevertheless held — measured, not argued

A draft row stored under a plural type cannot be promoted at all. promoteDraftForPublish addresses it by the folded singular, and SysMetadataRepository.whereFor emits that spelling verbatim with no at-rest fallback, so the promote raises NO_DRAFT and the all-or-nothing batch (ADR-0067 D2) aborts before Phase 2 ever runs. That is now pinned rather than assumed.

So the limb was unreachable — but nothing in the type system, the call graph, or any comment said why, and the helper's parameter list advertised the opposite. The safety of the arrangement rested on an unstated invariant three frames away, in a different file. This removes the parameter instead of restating the invariant.

The coverage gap is the sharper claim

The batch publish path had no coverage of the table-DDL side effect at all. Measured at fd6bdf89f: deleting the 'objects' limb and running the whole package gave 98 files / 1444 tests green — before and after. A green suite could not tell the two states apart, so "the existing publish/save coverage stays green" could not have licensed the deletion. The new test file closes that gap.

Precise about one thing: objectql's protocol-publish-package-drafts.test.ts did already touch this seam, but it stubs runPublishSideEffects out wholesale (mockResolvedValue({})) and pins the args, so it never exercised the DDL step. The claim above is about the side effect, not the seam.

Ablations — direction predicted BEFORE each run

#statepredictedmeasured
1ship state (fold + limb present)GREENGREEN — 4/4
2fold kept, 'objects' limb deletedGREENGREEN — 4/4
3guard inverted to accept ONLY 'objects'REDRED — 2 failed / 2 passed

Ablation 2 is the one that matters for follow-up: with the producer folding, the limb is genuinely dead, so its deletion becomes a true no-op provable by this file. Ablation 3 is the non-vacuity control — it fails as expected [] to deeply equal [ 'invoice', 'ticket' ], i.e. the tests really do observe this helper driving syncObjectSchema under the folded spelling, rather than passing for want of an assertion.

Deleting the limb is deliberately NOT in this PR. It is a separate, now-provable follow-up, so each step stands on its own claim. The dropObjectStorage twin at :11542 carries the identical limb and is untouched here.

One consumer followed the rename

packages/objectql/src/protocol-publish-package-drafts.test.ts asserted toMatchObject({ requestType: 'object', name: 'course' }) on the args seam. requestType no longer exists, so that assertion now reads singularType. The draft in that case is spelled singular either way, so it pins the same fact under the surviving field name — the expectation did not move, only the field name it reads.

This was a real red on Test Core (2/3), not a flake: my first verification ran only --filter @objectstack/metadata-protocol, while that shard carries objectql, a consumer of the changed package. The scoping error was mine; the consumer sweep below is the corrected verification.

Verification — union re-run at e1a06ad8f (current head, no commits since)

Consumer sweep (downstream direction — every package whose tests exercise ObjectStackProtocolImplementation), all green:

packagefilestests
metadata-protocol1021487
objectql2103672
rest1181948
runtime1632440
spec40210644
client23301
plugin-approvals23484
plugin-auth541238
plugin-email24375
dogfood110 (+1 skipped)779 (+3 skipped)

dogfood resolves the code under test from dist, so it ran against a rebuilt metadata-protocol — verified directly (requestType occurs 0 times in dist/index.js, built after the source commit).

  • metadata-protocol declares no typecheck script (it is a ledger package), so a green typecheck would say nothing here. Ratchet measured directly on a built workspace: pnpm check:type-check-debt --re-measure33 ledger entries re-measured, 1926 raw tsc errors, none above its recorded number. No ceiling raised. The one surplus it reports (@objectstack/lint, -1) is pre-existing and another package's ledger; left alone.
  • Gates re-derived from the real changed paths via scripts/pm/dispatch-gates.mjs, all green: check:cross-package-test-inputs, check:durability-log-level, check:filter-alias-parity, check:query-options-erasure, check:type-check-coverage, check:nul-bytes.
  • ⭐ Added beyond the dispatched list: check:engine-double-contract — this PR adds a fake engine, which no path derivation can name. Green at "264 pinned"; the double routes update/delete through assertEngineUpdateDispatch / assertEngineDeleteDispatch, so it is pinned and needed no ledger entry.

No changeset

No user-visible change: a private method's parameter list, a comment, and two test files. The PR declares no release of its own, which is the changeset gate's own stated criterion for exemption. The skip-changeset label was applied by the PM seat.

Out-of-scope findings filed while measuring, both unassigned: #8858 (batch audit row records the raw stored spelling) and #8862 (a third tolerance in this file that consumes the plural rather than only guarding on it — reachability explicitly unmeasured, and it overlaps #8819's live region).


Generated by Claude Code

… the producer
`runPublishSideEffects` took both a folded `singularType` and an unfolded
`requestType`, and `ensureObjectStorage` was the sole consumer of the
unfolded one — a spelling-tolerant lookup one layer below a boundary that
already folds, the shape `canonicalMetaType`'s header has rejected since
#4432.
Pass `args.singularType` and drop `requestType` from the parameter list
entirely, so no future consumer can reach for the unfolded value. Behaviour
is unchanged: `type` is read only by `ensureObjectStorage`'s guard and never
reaches `syncObjectSchema`, which takes `name` alone.
Also corrects the in-tree comment that claimed the `'objects'` limb was
unreachable because "both of its call sites stand behind a fold". That
reason was false — the second call site sits inside `runPublishSideEffects`,
whose batch caller does not fold. The conclusion held for an unstated
reason: a plural row at rest cannot be promoted (`whereFor` emits the folded
singular with no at-rest fallback), so the batch aborts with `NO_DRAFT`
before Phase 2. That is now measured rather than asserted.
Adds the batch publish path's first coverage of the table-DDL side effect.
Part of #8820
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
@vercel

vercelBot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 15, 2026 12:57pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/metadata-protocol)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/metadata-protocol)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/metadata-protocol)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/v9.mdx(via @objectstack/metadata-protocol)

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.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@hotlonghotlong added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/m tests labels Aug 15, 2026 — with Claude
@hotlong
hotlong marked this pull request as ready for review August 15, 2026 12:22
`protocol-publish-package-drafts.test.ts` pinned the args object
`publishPackageDrafts` hands `runPublishSideEffects`, asserting
`requestType: 'object'`. That field was removed in this branch —
`ensureObjectStorage` was its only consumer and now reads the folded
`singularType` every other consumer in the helper already read.
The draft in this case is spelled singular either way, so the assertion
pins the same fact under the surviving field name. The case stubs
`runPublishSideEffects` out entirely (`mockResolvedValue({})`), so it
pins the ARGS seam and never exercised the table-DDL side effect.
Part of #8820
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
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 gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ensureObjectStorage's 'objects' limb is now dormant — a spelling-tolerant lookup one layer below the /meta fold

2 participants

@hotlong@claude