Repository navigation
fix(metadata-protocol)!: an object saved back from a translated /meta read stores its source labels, and one the registry would refuse is refused at save and at publish - #29790
Conversation
… registration refusal before promotion Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd
… refusals, and the stored-row repair Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd
… by name Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd
…fusal parity Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd
…nd serves data Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd
…ho save door Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd
…storation runs after the refusals The injected-column half is pure and stays before the served-column strip; the half that reads the stored row now runs beside the credential carry-forward, so a refused save costs no read. A registry with no getItem has no code layer to read. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd
📓 Docs Drift CheckThis PR changes 3 package(s): 23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 171 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 c962e0d923565cb525e1947dd7780feb8f64f2eb && git checkout c962e0d923565cb525e1947dd7780feb8f64f2eb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0e4b0fa411de344a0c2f38500255c68fc1cdfdf2 1fc44a469f46d82643a528c0cd61cfca75af8737 && git checkout -B drift-repro 0e4b0fa411de344a0c2f38500255c68fc1cdfdf2 && git merge --no-ff 1fc44a469f46d82643a528c0cd61cfca75af8737
node scripts/docs-audit/affected-docs.mjs --json 0e4b0fa411de344a0c2f38500255c68fc1cdfdf2
|
|
domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf Contract-review-claim: session_01JQwxbXKRzgnVvjzxaYxLUf · head 866fe6c · 2026-11-03T04:55Z Face: narrowing ( Generated by Claude Code |
|
domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf Contract reviewServed-tier: Inputs read: card #29673 body and all 4 comments (triage, claim, claim amendment, os-dev-report); PR #29790 body; the net diff ① Derived judgments
② Semver levelChangeset
Clause-②: no (narrowing) — right in kind; the sentence overstates what the tree narrows (①.2 and ①.3): the SCHEMA face (spec addition): judged in ①.8 — right, additive, generated surfaces regenerated and matching. ③ Boundary flags
④ Design judgmentThe narrowing against the published spec on
Observations, no fix owed: boot judges the converted body and save the raw one (①.5) — no conversion touches the doors today, but a future field- Implemented-by: VERDICT: FAIL Generated by Claude Code |
…s source with the read's packaged base Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd
…ving the record-currency door to the registration Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd
…kaged base, answers on advisories, and the registry is asked after the spec gate The stored-row half translates its source against getPackagedObjectBase, the packaged base the REST read hands translateObject, and with no stored row takes that base as its source instead of the carry-forward's MetadataService code-layer read, so a degraded service fails no save. Each restored author slot is an info advisory on the save answer. The registry's refusal is asked after the spec gate, so a body both refuse keeps the gate's issues. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd
|
domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf Contract reviewServed-tier: Delta record on the FAIL record 6595971055 rendered on ① Derived judgmentsEach finding of 6595971055, against the tree at this head:
Unchanged from 6595971055 and still right: one code path for pre-flight and registration ( ② Semver levelChangeset still ③ Boundary flags
④ Design judgmentThe narrowing is as judged on 866fe6c: it makes ADR-0142 D3's and ADR-0029 D9.9's refusals real at the producer and removes no declared capability. The one defect that failed the previous head — a name-keyed restoration undoing an author's edit on a slot the read never translates — is closed at its cause (the module now reads with the read's own options), and the restoration is loud on the save answer. No new finding. The design holds. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf 维护者速读
席位核对ACCEPT ·
Generated by Claude Code |
… round-trip item The 18.3.0 wave-5 re-check (QA runs #29890, #29893, #29877 and #29870, at 589049b) found checklist steps, verifies and fixtures that no longer match the product. Each line was re-read at base f68b47c before it was applied. - Part A (W5-FAIL): 19 items, among them the action door for the aggregate bulk action, the ADR-0146 D11 pair recipe for staging a shipped view or email template, the 'Switched on / off' wording, the Invite outcome copy and the per-group approval read. - Part B (W5-STUDIO): 24 studio-authoring items, among them the Studio routes for the picklist fixture and the default-currency prompt. - Part C (W5-P0): first-run-loop knownGaps[3] is a regression row since #29790, and RUNNER's console-auth fact names the Packaged Actions page. - Part D (W5-EXPORT): new item platform-core.package-export-round-trip. - RUNNER: the overlay fact now gives the D11 pair recipe, and a new fact covers the first-page currency / timezone prompt. - platform-core.manifest-install-contract source[1] is re-pointed, because its anchor no longer resolved and the checker refused the area file. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
Requested by Warren · project thread
Fixes #29673
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none —
skills/objectstack-data/SKILL.md(Ownership queues) already teaches that an object declaring its ownqueuefield is refused, which stays true and is now said at save; no file underskills/**teaches saving a translated read back.维护者速读
PUT /api/v1/meta/object/NAME,草稿与直接保存)把"中文读取被翻译过的标签"还原成源文本再存,并在保存时就拒绝注册表本会拒绝的对象(422),发布前再查一次;os migrate meta --stored可修复已存的坏行。queue的标签是"队列",原样保存后被当成作者自定义字段,注册表拒绝注册,但发布仍报"成功",数据接口 404;owner_id等列的中文标签还会被所有其他语言的用户看到。queue字段等注册表本就拒绝的对象,现在保存即 422(以前保存成功但永远注册不上);作者恰好输入某语言对源标签的译文时会被还原为源文本。回滚即还原本 PR,无数据迁移。packages/metadata-protocol/src/protocol-translated-echo.ts(还原规则)、packages/metadata-protocol/src/protocol.ts中saveMetaItemUnaudited的两处还原调用(restoreInjectedColumnEchoes、restoreTranslatedObjectEchoes,后者以getPackagedObjectBase为打包基线)与规格校验之后的objectRegistrationRefusal、packages/objectql/src/registry.ts的judgeRegistrationDoors(原有拒绝逻辑整体搬移)。pnpm --filter @objectstack/objectql exec vitest run src/protocol-meta-effective-schema.test.ts,预期 20 个测试全绿,其中[#29673]一节 8 个。What changes
The door fixed:
saveMetaItem(packages/metadata-protocol), the server door every objectPUT /api/v1/meta/object/NAMEreaches, draft and live. Whatever the client sends, an injected column or a field no longer stores a label the read translated.Where the translation came from (measured, below): the REST
/metaread runstranslateMetadataDocument, which serves each injected system column's label from the built-in table with no bundle needed (queue→队列,owner_id→负责人) and an author field's label from the catalog. Studio saves the whole body it read. The save door stored it verbatim;stripInjectedSystemColumnsremoves only a byte-identical platform definition, so the translatedqueueandowner_idstayed as the author's own fields. The registry refuses an authorqueueon a user-owned object,applyObjectRegistryMutationcaught that refusal into aconsole.warn, andpublish-draftsansweredoutcome: 'published'.protocol-translated-echo.ts, new).saveMetaItemUnauditedcalls it twice: a pure half (restoreInjectedColumnEchoes, the platform's injected-column definitions only, no read) just beforestripServedObjectColumns, and the stored-row half (restoreTranslatedObjectEchoes) after every refusal, besidecarryForwardRedactedCredentials, so a refused save costs no read. A slot is restored when the incoming value differs from its source and equals whattranslateObjectmakes of that source in some locale. Sources, asked in order: the stored row this save overwrites (the draft, else the active row a first draft overlays, read as the carry-forward reads it), else the packaged base (getPackagedObjectBase), then the platform's definition of an injected column (injectedSystemColumnDefs). Every read of the source is translated against that packaged base, passed astranslateObject'spackagedBaseexactly as the REST read passes it, so a packaged object's overlay slot that diverged from the packaged text (served untranslated) is no translation's source. The packaged base is a registry lookup, not a MetadataService read, so a degraded metadata service fails no save here. Locales: the registered bundle's, plus the built-in table's (BUILTIN_SYSTEM_FIELD_LABEL_LOCALES, a new export of@objectstack/spec/system), since the read applies that table with no bundle. Slots: objectlabel/pluralLabel/description; fieldlabel/description/inlineHelpText/inlineTitle; optionlabel. Once restored, the injected columns are byte-identical again and the existing strip removes them.isInjectedColumnDefinitionis unchanged.SchemaRegistry.objectRegistrationRefusal(new,packages/objectql/src/registry.ts) answers the errorregisterObjectwould throw, registering nothing.registerObject's doors moved whole into a privatejudgeRegistrationDoors, whichregisterObjectnow calls first, so the question and the throw are one code path. It judges the body alone: the record-currencysummarydoor, which reads the registry's other objects as they stand, is left to the registration (abodyOnlystop injudgeRegistrationDoors), since a sibling drafted in the same batch can change its answer either way. The save door asks it in both modes, after the spec gate (so a body both refuse keeps the gate's structuredissues[]):422 INVALID_METADATA, nothing stored. The one refusal this makes new at the door is an author's ownqueueon a user-owned object; a fieldtypethe registry refuses was already refused by the object schema.promoteDraftForPublishasks the same question inside its D9.9 gate block, beforerepo.promoteDraft. A draft stored before this rule is refused there: pinned onpublishMetaItem(refused by name, no active row, nothing registered); inpublishPackageDraftsthe promote gates' throws land infailed[], so the receipt does not report it published.migrateStoredMetadata(os migrate meta --stored [--apply]) runs the same restoration on each stored object row (comparing the row with the platform's definition) and reports each slot astranslated-label-echo, e.g.fields.queue.label 队列 → Queue.--applyre-saves the row through the save door, which restores and strips. A restart does not heal such a row; the migration does, and so does the row's next save.infoadvisory on the save answer (rule: 'translated-label-echo',path: 'objects.NAME.fields.F.label', with the text stored and the text sent), beside the credential carry-forward'swithheld-key-droppednotices. The platform's injected columns are not reported: the stored document never carries them.Out of #29414's region. The
promoteDraftForPublishhunk sits inside the D9.9 gate block only; nopublishPackageDraftscommit-plan rows,prevVersion/existedBeforerecording,revertCommitorresolveOverlayPackageBindingline is touched.A ui/ change is also owed (the seat files the card).** Studio's object designer edits the locale-translated read:
ui/packages/auth/src/createAuthenticatedFetch.tsattachesAccept-Languageto every API call, so the designer'sGET /api/v1/meta/object/NAMEreturns translated labels and itPUTs that body back. With this PR the stored document is right, but the designer still shows a translated label as editable source text, so an author who edits it in zh-CN writes a Chinese source label (kept, correctly, as the author's edit). The owed card: the designer loads and edits the untranslated document, and does not send injected system columns back.Files (12):
packages/metadata-protocol/src/protocol-translated-echo.ts(new),protocol.ts: the restoration, the save and promote refusals, the migration notices.packages/objectql/src/registry.ts:objectRegistrationRefusal,judgeRegistrationDoors(moved doors, no rule changed;bodyOnlystops before the record-currency door).packages/spec/src/system/i18n-resolver.ts:BUILTIN_SYSTEM_FIELD_LABEL_LOCALES;api-surface/system.jsonandexport-origins/system.jsonregenerated.protocol-translated-echo.test.ts(new),protocol-meta-effective-schema.test.ts,ownership-queue.test.ts,record-currency-guards.test.ts,packages/qa/dogfood/test/studio-zh-cn-echo-publish.dogfood.test.ts(new)..changeset/29673-translated-echo-save-door.md: patch for spec, objectql, metadata-protocol;!with the ADR-0087not-requireddisposition.Tests
packages/objectql/src/protocol-meta-effective-schema.test.ts,[#29673](realSchemaRegistry+ real protocol):amount,memo;memokeeps its author label; the registry has the object; anenread is servedOwner/Queue.queueis refused at the save door in both modes (ObjectQueueColumnRefusedError,INVALID_METADATA, 422), and no row is written.queue:publishMetaItemis refused by that name, no active row, nothing registered.pendingwith the twotranslated-label-echonotices; applyrewritten; the stored fields areamountonly and the object registers.amountserved金额) is stored asAmount, and the save answer carries oneinfotranslated-label-echoadvisory atobjects.crm_invoice.fields.amount.label.Subjectdiverged from the packagedTitle, with the author typing the catalog string标题, is stored标题with no advisory; the control with no packaged base restoresSubject.type) answers the spec gate'sINVALID_METADATAwithissues[].packages/metadata-protocol/src/protocol-translated-echo.test.ts(6): every slot in zh-CN;ja-JPwith no bundle; author text kept; a stored echo repaired against the platform definition; the arrayfieldsshape, and the same reference back when nothing changed; a diverged overlay slot set to the catalog string is kept when the packaged base is passed, and the undiverged control is restored.packages/objectql/src/ownership-queue.test.ts:objectRegistrationRefusalanswers the throwregisterObjectmakes, and registers nothing.packages/objectql/src/record-currency-guards.test.ts:objectRegistrationRefusalanswersnullfor a parent whosesummaryreads a record-currency child, whileregisterObjectstill refuses it.studio-zh-cn-echo-publish.dogfood.test.ts(4, over the booted showcase through the REST doors): the zh-CN read is translated and theenread is not; the zh-CN echo publishespublished/failedCount: 0, stores neither column, serves data, and anenread getsOwner/Queue; theencontrol; an authorqueueis refused 422 and no draft is stored.Acceptance notes
422 INVALID_METADATAan object declaring its ownqueuefield on a user-owned object (ADR-0142 D3), which it stored before and the registry then refused to register; the promote refuses such a draft. A label equal to what the/metaread serves for its source in some locale is stored as the source. Nothing is widened.summarydoor, which reads the registry's other objects as they stand, stays the registration's own question in both directions: a draft whose sibling in the same batch takes the child's amount out of record-currency mode is not refused at save, and one whose sibling completes the pair passes save and promote and fails at registration, whereapplyObjectRegistryMutationstill only logs it. Turning that catch into afailed[]row means changing the Phase 2 receipt, which is metadata-protocol: a pair commit's revert beside a pre-close base-bound row answers success while writing nothing, because the commit plan and the restore both read any package's active row #29414's region; left for a follow-up after it lands.translateObjectwrites for the object and its fields. Translated list-view labels and validation messages are not compared; none of them is a registration refusal.protocol.tswas frozen from 2026-11-03 03:39 UTC until the [maintainer] refactor(metadata-protocol): splitsrc/protocol.ts(33,065 lines, +10,169 in 30 days) along itsObjectStackProtocolImplementationseams, one move-only slice per PR #25467 section-3 PR merged (339f63d). Main was merged into this branch after the lift with no conflicts, and the review fixes that touchprotocol.tslanded after it (1fc44a4).Accept-Language: zh-CN): create the object in the designer, publish, and expect (a) the publish receiptpublished, (b) the new object's list serves records, not 404, (c)GET /api/v1/meta/object/NAME?layers=truehas noqueue/owner_idin the overlay, (d) the same read inenshowsOwner/Queue.Verification
Red first: measured on main fc55892, booted showcase, zh-CN vs en
Green: same script on this branch
Ablations (scripts/ablation-replace.mjs + ablation-dist-preflight, metadata-protocol dist rebuilt per leg)
protocol-meta-effective-schema.test.ts2 failed / 14 passed (the zh-CN echo and the migration repair).sharingModeland the pin assertsObjectQueueColumnRefusedErrorby name, and the rerun was red.Final head 1fc44a4 (after contract review 6595971055 and delta review): gates, lint, package tests, ablations, dogfood battery (exit codes captured to file)
dispatch-gates --commands: 85 derived; all 85 exit 0.--ranreconciliation: 85 run, 0 NOT-MEASURED. (check-plugin-teardown-shape --self-testneededgit fetch origin 621a4876in the shallow clone;check:dual-build-cjs-loadsneeded a whole-workspaceturbo run buildfirst.)pnpm lint(eslint over the repo, thenturbo run typecheck): exit 0.turbo run build: the first run failed only in@object-ui/app-shellonpinyin-pro, a dependency main added, missing fromnode_modulesafter the merge; afterpnpm install --frozen-lockfilethe app-shell build ran 65/65 tasks successful.pnpm --filter @objectstack/metadata-protocol test: 311 files passed, 4 skipped; 22311 tests passed, 26 skipped.pnpm --filter @objectstack/objectql test: 544 files, 10770 tests passed.pnpm --filter @objectstack/spec exec vitest run src/system: 69 files, 2213 tests passed.pnpm --filter @objectstack/dogfood test(the full battery, narrowing arm): 406 files passed, 3 skipped; 3356 tests passed, 28 skipped.scripts/ablation-replace.mjs+ablation-dist-preflight, metadata-protocol dist rebuilt per leg, marker present in the built files), each red as predicted and restored to blob == HEAD:queuerefusal, 1 of 20;translateObject: 1 of 6;bodyOnlystop removed: the record-currency pre-flight test, 1 of 14.getItem/engine.find. 866fe6c moved that read after the refusals; 1fc44a4 replaced its code-layer fallback with the packaged base, and the interimgetItemguard inreadCodeLayerForCarryForwardwas reverted.🤖 Generated with Claude Code
https://claude.ai/code/session_01JQwxbZUVFvbHQ7Uq4qpCdd