Repository navigation
The /meta read exits materialize the FOLDED object, the registry materializes the BASE — so an extended title-less object gets a __search column the registry never provisioned #8376
Description
Filed by the #8268 dev agent, measured while enumerating the stamps that cross the /meta read-exit materialization seam. Unassigned. This is a position defect of that seam, not a missing stamp — which is why #8268 did not fix it and deliberately preserved the existing behaviour rather than re-spelling it.
Symptom
An object whose BASE layer has no title-eligible field, extended by an extend contributor that adds one, is served a __search companion column by both /meta reads while registry.getObject(name) has none:
registry.getObject(name).fields => project, seats, nickname, ... (no __search)
GET /meta/object/:name .fields => project, seats, nickname, __search, ...
GET /meta/object .fields => project, seats, nickname, __search, ...
The measurement
Reproduces on the absent host — no metadata service at all, so the read falls through to the registry. That is the tell: this is not the artifact-ingest seam #8038/#8268 are about. Both /meta reads disagree with the registry on an object the registry itself produced.
absent host, untitled base + extend contributor adding a text field:
byName vs registry diverge on keys: fields
listed vs registry diverge on keys: fields
differing key: __search byName: present registry: absent
Control, same run: a base that does carry a title-eligible field agrees on every key with an extender present (diverge on keys: (none)), so the divergence tracks the base-vs-folded distinction specifically and not the presence of extenders.
Root cause
The two run the same provisioning at different points in the pipeline:
SchemaRegistry.registerObjectmaterializes the BASE layer — the ADR-0079 gate isownership === 'own' || 'overlay', and the comment there says it outright: "extensions must not redesignate the owner's title".resolveObjectthen foldsextendcontributors on afterwards and never re-materializes.- The
/metaread exits apply the convergence at the EXIT, by which pointfoldObjectExtendersFromRegistryhas already folded the extenders on. SoprovisionSearchCompanionOntoresolves its source field over the folded field set and finds the extension's text column.
So the registry answers "this object has no title, therefore no companion" and the read answers "this object has a title contributed by an extension, therefore a companion".
Why #8268 did not fix it
#8268's dispatch scoped the work to the read-exit stamps and explicitly forbade re-spelling the mechanism #8038 built. Relocating the convergence from the exit to the pre-fold position is exactly that re-spelling, and it has real blast radius: the OVERLAY list path depends on the exit-side convergence (meta-object-search-companion-agreement.test.ts pins that the list read converges an overlay row, and says a revert makes that case alone go red in the listed half). So it needs its own card.
#8268 did make sure it does not GROW the problem: its nameField convergence withholds a designation the registry itself declined, precisely so the title stamp does not acquire this defect too. materializeServedObjectOnto's docstring records that asymmetry and points here.
Which answer is right — worth ruling explicitly
Both readings are defensible and the fix differs:
- The registry is right (materialize the base, extensions never contribute a title or a companion): then the read exits must materialize BEFORE the fold, and the
__searchcolumn disappears from these objects. - The read is right (the resolved object genuinely has a searchable text column, so it should have a companion): then
resolveObjectmust re-materialize after folding, and the registry — and therefore the driver'ssyncSchema, which creates the real column — gains one.
Where (anchored on function names, not line numbers)
packages/objectql/src/registry.ts—registerObject(materializes the base),resolveObject/foldExtendersOntoDefinition(folds afterwards, no re-materialization),materializeServedObjectOnto(the read-exit replay, and the docstring recording this gap).packages/metadata-protocol/src/protocol.ts—foldObjectExtendersFromRegistryruns beforegovernServedObjectat each exit; that ordering is the defect.
Reproduction
new SchemaRegistry({ searchCompanion: true }); register a junction object whose fields are onlylookup+numberasownership: 'own'.- Register a second contributor for the same name as
ownership: 'extend'carrying onetextfield. - Read
GET /meta/object/:name(nometadataservice needed) and compare withregistry.getObject(name). - The served body carries
__search; the registry's does not.
Pinned as an EXPECTED divergence in packages/rest/src/meta-object-materialization-agreement.test.ts (MEASURES the companion over-provisioned on an extended title-less base), so a fix here turns that pin red and should update it in the same PR.
Dedup
GitHub search over open issues for companion provisioning / extender fold / registry disagreement found no twin (nearest: #3955, closed, migrate-CLI vs dev-runtime companion disagreement — a different pair of producers). nameField on this seam is #8268; the multi-tenant indexes stamp is its sibling finding.
Related
#8268 · #8038 (installed the exit-side convergence) · #7556 / #8027 (the extender fold at the read exits) · ADR-0029 D9 · ADR-0045 · ADR-0079
Generated by Claude Code
Activity
test(rest): reference the two filed findings (#8375, #8376) from the …
os-zhuang commentedon Aug 13, 2026
Decision card — routed to the maintainer, not ruled by the seat
Labelled
needs-user-decision. Recording why this is not a seat call and what the two branches actually cost, so it can be ruled without re-deriving anything.The divergence
SchemaRegistry.registerObjectmaterializes the base layer.resolveObjectfoldsextendcontributors on afterwards and never re-materializes. The/metaread exits then transform the already-folded document.So the two sides materialize at different points in the pipeline, and a title-less base that an
extendcontributor gives a text field to gets a__searchcolumn from both/metareads and none fromregistry.getObject(). Same object, same host, same moment.It reproduces on a registry-only host, so this is not the artifact-ingestion seam that #8268 fixed — it is a distinct defect that has been live since #8038 and is unchanged by PR #8380. #8380 deliberately did not act on it, correctly: relocating that convergence is the re-spelling #8268 was told not to do, and the overlay list path depends on the current placement (#8038's own test shows a revert makes that case alone go red).
Why the seat is not ruling it
syncSchemacreate real__searchcolumns on objects that have none today — an ADR-0045 additive migration against existing tenant data. Migrations against live tenant data are the maintainer's call; this seat has consistently routed them up rather than absorbing them.extendcontributor influence a designation the owner's base did not make? That is architecture, not implementation sequencing.The two branches
resolveObjectre-materializes after foldingsyncSchemacreates real__searchcolumns on objects that have none today (ADR-0045 additive migration)registerObject's stated intentRecommendation on record (informed, not binding)
The dev who measured it recommends branch 1: it is the non-migration-bearing option and it matches
registerObject's stated intent that extensions must not redesignate the owner's title. I am recording that as an informed recommendation rather than adopting it, because adopting it is exactly the call I am saying is not mine.What is needed
A ruling on which side is authoritative. Until then this stays
needs-user-decisionand is not dispatched; the divergence is pinned as an EXPECTED failure in PR #8380's new test, so whichever way it is ruled, the pin fails deliberately rather than drifting silently.Generated by Claude Code
hotlong commentedon Aug 13, 2026
Triage supplement — four-prong card-face block (required on every
needs-user-decisioncard; the seat's decision comment above carries the full analysis, this serializes it):registerObject's stated ADR-0079 intent. Branch 2 grows the surface: fold-then-re-materialize becomes a second materialization pass every consumer must reason about.__searchcolumn today./metacannot be told a column exists thatsyncSchemanever created (today's shape is exactly the silent declared≠provisioned trap). Branch 2 repairs the same lie in the opposite direction but at the cost of implicit schema growth an author never declared.All four prongs point at branch 1, consistent with the recorded dev recommendation — but this card sits on the manual floor twice over (live-data migration shape on one branch; extension-authority semantics), so it stays in the inbox for the maintainer regardless of prong alignment.
Generated by Claude Code
hotlong commentedon Aug 13, 2026
Maintainer ruling — Branch 1: the registry is authoritative
Ruled in a live PM session, 2026-08-13, accepting the triage seat's four-prism recommendation in full (maintainer verbatim: 「接受你的全部建议」). Recorded by the triage seat as a ruling record.
Ruling: branch 1.
SchemaRegistry's base-layer materialization is the authoritative answer. The/metaread exits must materialize before the extender fold, so the served document stops carrying a__searchcompanion the registry never provisioned andsyncSchemanever created. Extensions do not redesignate the owner's title — matchingregisterObject's stated ADR-0079 intent.⛔ Branch 2 is rejected, on the asymmetry the card itself identified: it is schema-migration-bearing (ADR-0045 additive columns against existing tenant data) and it repairs a declared≠provisioned lie by growing the schema instead of removing the divergence.
Prism basis: single materialization point (coherence); zero measured pull, so the cheaper branch wins on tie-break; a served document that equals what was provisioned is the closed answer an AI author cannot be misled by; and no migration beats a migration.
Binding implementation constraints for the metadata seat:
meta-object-materialization-agreement.test.ts, the "companion over-provisioned on an extended title-less base" case) turns red by design — update it in the same PR, and state in the PR body that its inversion is the deliverable's proof.nameField's deliberate withhold asymmetry (recorded inmaterializeServedObjectOnto's docstring, pointing here) should be re-read at implementation time: once this lands, that docstring's note about the gap needs updating too.State:
needs-user-decision→pm:queue. Blocked-by: none (checked live — the referenced sibling PR has landed).Generated by Claude Code
os-zhuang commentedon Aug 14, 2026
Claim — metadata seat, implementing the 2026-08-13 ruling (branch 1)
session_012WMpuAfA2KSdDjGF6tm1bHclaude/issue-8376-materialize-before-fold../objectstack-issue-8376packages/metadata-protocol/src/protocol.tsregion declared (file is capped at 3 concurrent editors): the/metaobject read-exit materialization seam —foldObjectExtendersFromRegistry/governServedObject/materializeServedObjectand their call sites at the byName, listed and effective-schema exits. Not touchingpublishPackageDrafts(held by the #8400 seat).Also expecting to touch
packages/objectql/src/registry.ts(materializeServedObjectOnto's docstring, per the ruling's third constraint) and the twopackages/rest/src/meta-object-*-agreement.test.tspins.Working to the ruling as written: materialize before the fold; the overlay list pin must stay green or I stop and report the fork; the EXPECTED-divergence pin inverts in the same PR as the deliverable's proof.
Generated by Claude Code
Generated by Claude Code
os-zhuang commentedon Aug 14, 2026
{ "issue": 8376, "status": "done", "branch": "claude/issue-8376-materialize-before-fold", "pr": "https://github.com/objectstack-ai/objectstack/pull/8614", "premise_still_valid": true, "summary": "Implements the 2026-08-13 ruling, branch 1. The /meta 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 column the registry never provisioned. The seam now defers each such stamp to the verdict the registry reached over the BASE at registration, generalising the nameField withhold #8268 installed for one stamp into the seam's one rule. IMPORTANT DEVIATION IN MECHANISM, NOT IN OUTCOME: the ruling's literal 'move the convergence before the fold' is not reachable as a call-site reordering, and I measured why rather than assuming. On the card's own reproduction host (registry-only) no protocol-side fold runs at all — resolveObject folded the body inside the registry before the protocol saw it — so there is no exit-side call to reorder; and the exit cannot un-fold, because subtractExtenderContributions reaches validations/indexes only while fields is a key-keyed spread with no inverse (an extender may replace a base field under the same key). Both facts measured with a standalone probe. The ruling's stated outcome is delivered exactly: the served document stops carrying a companion the registry never provisioned, with no schema migration and no object gaining a column. No ledger grew; no call site moved.", "tests": "Baseline on origin/main: rest 114 files / 1881 tests passed (both agreement suites green, the #8376 case pinned as an EXPECTED divergence). Post-fix, post-merge with origin/main: objectql 202 files / 3559 tests passed; metadata-protocol 86 files / 1263 tests passed; rest 115 files / 1896 tests passed. Typecheck green for all three. CONSTRAINT 1 MEASURED BY NAME in a verbose run: '✓ agrees on an OVERLAY-backed host — the third link, and the only one the LIST exit converges' — the overlay list pin survived; no fork to report. Four ablations, each predicted before running. A1 revert the whole registry change: predicted 1 red (the inversion), measured 1 failed / 17 passed, 'expected [...] to not include __search'. A2 disable only the companion withhold: predicted 1 red, measured 1 failed / 17 passed. A3 disable only the primary withhold: predicted 2 red incl. the inversion failing on its divergingKeys line rather than its companion lines, measured 2 failed / 16 passed with \"expected 'nickname' to be undefined\" and \"expected ['nameField'] to deeply equal []\" — prediction matched including the reason. A4 neutralise the exit-side convergence entirely: predicted broad artifact-host reds plus the OVERLAY case red in its listed half plus the inversion staying GREEN; measured 9 failed / 9 passed, overlay red exactly at expect(host.listed).toContain(SEARCH_COMPANION), inversion green — direction right but I UNDER-COUNTED (predicted 6-7, measured 9): two absence-asserting cases also carry a whole-document divergingKeys assertion which moves when nameField is lost. Gates: union re-derived from the final diff with scripts/pm/dispatch-gates.mjs and re-run after merging origin/main — 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 all pass; check:type-check-debt passes in both --self-test and --re-measure (the latter only after building the full workspace closure, which the script correctly refuses to run without). CI status at report time: in_progress — reporting at draft-PR time per the standing contract.", "open_questions": [ { "question": "The ruling's wording is positional ('the read exits must materialize BEFORE the extender fold'). That is not implementable as a call-site move — measured: the reproduction host runs no protocol-side fold, and `fields` has no inverse to un-fold with. I delivered the ruling's stated OUTCOME by making the seam defer each document-sensitive stamp to the registry's own pre-fold verdict. Does the maintainer want to confirm this reading before the PR is enqueued?", "options": [ "A — accept as landed: the registry is authoritative, so taking the pre-fold verdict from the registry IS the ruling's semantics; the mechanism differs from the wording only because the wording assumed a call site that does not exist on the defect's own host.", "B — require a literal repositioning: this would mean changing what the registry hands the read path (serving a base layer plus a separate fold at the exit) rather than touching the exits, a much wider change across every /meta consumer, and it would put the overlay list pin at risk — the exact fork the ruling told me to stop on." ], "recommendation": "A. B trades one divergence for another and is the blast radius #8268 was told not to take on; A leaves every call site untouched, which is why the overlay list pin survived structurally rather than by luck. The PR body states the deviation openly rather than presenting the fix as a repositioning." } ], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
fix(metadata-protocol,objectql): the /meta read exits materialize a s…
fix(objectql): materialize the /meta object seam over the BASE layer,…