Skip to content

fix(objectql): materialize the /meta object seam over the BASE layer, never the folded document (#8376) - #8614

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-8376-materialize-before-fold
Aug 14, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/issue-8376-materialize-before-fold

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #8376

Implements the maintainer ruling of 2026-08-13 — branch 1, the registry is authoritative. Branch 2 (re-materialize after folding, growing syncSchema's column set) was rejected as schema-migration-bearing and is not attempted here.

The deliverable's proof is a pin INVERTING

packages/rest/src/meta-object-materialization-agreement.test.ts carried this divergence as an EXPECTED failure — it MEASURED the defect rather than fixing it, precisely so the day it was fixed the file would fail and be updated deliberately instead of drifting. That day is this PR. The case renamed from

  • MEASURES the companion over-provisioned on an extended title-less base — filed as #8376

to

  • never PROVISIONS a companion the registry itself declined — the #8376 inversion

and its two load-bearing assertions flipped:

assertion before after
byName.fields .toContain('__search') .not.toContain('__search')
divergingKeys(byName, registryResolved) ['fields'] []

That inversion is the deliverable's proof. The rest of the file staying green is the proof that it cost nothing.

Why this is not a call-site move

The ruling asks the read exits to materialize before the extender fold. Measured, that turns out not to be reachable as a call-site reordering, and the reason is worth recording:

  1. On the host that reproduces the defect there is no exit-side fold to reorder. The card's repro is the registry-only (absent) host. There the served body comes from resolveObject, which folded it inside the registry before the protocol ever saw it — foldObjectExtendersFromRegistry never runs on that path. Measured directly: registry.getObject(name) has no __search, and materializeServedObjectOnto applied to that same body adds one. That call is the sole producer.
  2. The exit cannot un-fold either. The fold's idempotence (subtractExtenderContributions) reaches validations and indexes only. fields is a key-keyed spread with no inverse, because an extender may legitimately replace a base field under the same key. Measured: after foldObjectExtendersOnto, the extension's field is still in the map.

So the seam instead refuses to re-decide a document-sensitive stamp over the folded document, and defers to the verdict the registry reached over the base at registration. That is the ruling's semantics — the registry is authoritative — and it is the same mechanism #8268 already installed for nameField, now stated once as the seam's one rule rather than per stamp.

materializeBaseLayer gains a provisionCompanion withhold beside the existing designatePrimary one; materializeServedObjectOnto asks declinedBaseLayerStamps for both. The docstring records the test for stamp five: a stamp needs a withhold only if its decision reads the document. #8375's tenant-scope index does not (deployment flag alone), which is why it has no entry and needs none.

Constraint 1 — the OVERLAY list pin survived, measured

Green, by name, in a verbose run:

✓ agrees on an OVERLAY-backed host — the third link, and the only one the LIST exit converges

It survives structurally: no call site moved, so the exit-side convergence the overlay list path depends on is untouched. Ablation A4 below re-measures that dependency and confirms it is live rather than assumed.

Prediction vs measurement — every ablation

Predictions were written down before each run.

# ablation predicted measured match
A1 revert the whole registry change, keep the new pins 1 red — the #8376 inversion; control and overlay pin green 1 failed / 17 passed, exactly the inversion (expected [...] to not include '__search') yes
A2 disable only the companion withhold 1 red — the same inversion 1 failed / 17 passed, the inversion yes
A3 disable only the primary withhold (the #8268 limb) 2 red — never INVENTS a designation… and the inversion, the latter on its divergingKeys line, not its companion lines 2 failed / 16 passed; expected 'nickname' to be undefined, and expected ['nameField'] to deeply equal [] yes, incl. the reason
A4 neutralise the exit-side convergence entirely broad reds across the artifact-host cases plus the OVERLAY case in its listed half; the #8376 inversion stays GREEN 9 failed / 9 passed; overlay red at expect(host.listed).toContain(SEARCH_COMPANION); inversion green direction right, count wrong — predicted 6–7, measured 9

The A4 divergence, stated rather than smoothed: I under-counted. Two cases I expected to survive — serves no companion … when the DEPLOYMENT has companions off and stamps NO tenant index on a SINGLE-TENANT deployment — went red as well. Both assert absence, which is what misled me; both also carry a whole-document divergingKeys assertion, and on the artifact host the un-converged body loses nameField, so the class-level assertion fails even though every absence assertion in them holds. Useful correction: on this file the class-level assertion is the one that moves, not the named-stamp ones.

A4 also produced the most useful negative result: the #8376 inversion pin stays green when the exit-side convergence is removed wholesale. So that pin alone does not prove the fix is narrow — it would pass for a fix that simply stopped converging anything. The narrowness is carried by the controls, which is why this PR adds one.

Anti-vacuity — how I know the harness reaches the fix

  • A1/A2 show the inverted pin is genuinely wired to the change: revert either the whole fix or only the new limb and it goes red, with the assertion text naming __search.
  • A3 shows the retained GET /meta/object/:name drops nameField for artifact-ingested objects — the third materialization stamp to diverge on this seam #8268 limb is not dead code under the new mechanism — removing it reddens two cases, one of them for the predicted reason.
  • The inverted pin asserts non-vacuity from inside: the fold really happened (nickname is present in the served fields), and the extension's field really is a title-eligible companion source (type === 'text'), so withholding is a decision and not an inability. Without those two lines the case would pass on an object that simply has no extension.
  • New control added — KEEPS provisioning the companion when the base itself carries the source, extension or not. Same shape as the withheld case (extended, companions on), differing only in that the base carries the title-eligible field itself. This is the exact trap the ruling named — do not trade one divergence for another — and a fix that withheld the companion whenever an object had an extender at all passes the inversion and fails this.
  • A pin's name is not its reach, so the overlay claim was checked by running it verbose and reading its own name in the pass list, and A4 confirms it can actually go red.

Verification

Full suites, post-merge with origin/main:

packages/objectql          202 files / 3559 tests passed
packages/metadata-protocol  86 files / 1263 tests passed
packages/rest              115 files / 1896 tests passed

Typecheck green for all three. Gate union re-derived from the final diff with scripts/pm/dispatch-gates.mjs and re-run after the merge — all pass: check:nul-bytes, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:durability-log-level, check:objectui-changeset, check:query-options-erasure, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-cross-package-test-inputs, check-engine-split-ratio. check:type-check-debt passes in both --self-test and --re-measure (the latter after building the full workspace closure, which it correctly refuses to run without). No ledger grew.

The nameField docstring (ruling constraint 3)

materializeServedObjectOnto's docstring carried the note that the same base-vs-folded asymmetry applied to __search and was deliberately not closed, pointing here. That note is now wrong, so it is gone. It is replaced by the general rule the two stamps now share, the measured reason un-folding is not available, and the test to apply to stamp five. provisionSearchCompanionOnto is untouched — it remains the pre-#8268 shim for partial registry doubles, and its own docstring already says the read exits no longer call it.

No schema migration: no object gains a column it did not already have.


Generated by Claude Code

claude added 3 commits August 14, 2026 00:57
…the folded document

The read-exit replay held an already-folded object and re-decided every
document-sensitive materialization stamp over it, so a title-less base that an
`extend` contributor gives a text field to was served a `__search` companion
the registry never provisioned and the driver's syncSchema never created.

Generalises the `nameField` withhold #8268 installed for one stamp into the
seam's one rule: a stamp whose decision reads the document is withheld when
this registry resolved the object and its own answer does not carry it.

Fixes #8376

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

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 14, 2026 2:10am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/objectql)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/system-context.mdx (via packages/objectql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)

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

  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql)

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.

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/m tests tooling

Projects

None yet

2 participants