Skip to content

fix(rest): the by-name read of a view container answers 200 on an artifact boot and translates the views inside it (#22614) - #22687

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22614-meta-view-container-read
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22614-meta-view-container-read

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22614

Clause-②: no

A 500 is a fault, not a refusal. No accepted set, export or error code changes. The by-name read of a view container now answers on every boot, translated.

The defect, re-measured on this branch's base (3d0eeefa4)

One single-package project: an object acme_note, a defineView({ object: 'acme_note', list, listViews: { mine } }) container, and a zh-CN catalog for both views. The read is GET /api/v1/meta/view/acme_note, authenticated as the seeded admin.

boot before after
artifact (os build, then os serve of the artifact) 500 INTERNAL_ERROR; log: Cannot read properties of undefined (reading 'startsWith') 200; list.label is 全部笔记, listViews.mine.label is 我的笔记
config (os serve objectstack.config.ts) 200, but label: "acme_note" is served (no author wrote it), and both views stay All Notes / My Notes for a zh-CN reader 200; the same container as the artifact boot serves
control: GET /meta/view/acme_note.default, /acme_note.mine 200, translated unchanged, on both boots

Why the two boots served different documents. Both registrars file the container under its object. The config boot's ObjectQL boot loop stamps the derived key onto the item (packages/objectql/src/engine.ts:7575, toRegister = item.name === itemName ? item : { ...item, name: itemName }). The artifact loader registers the container as shipped, with no name (packages/metadata/src/plugin.ts:1198). The read handed either one to the spec's single-view translator (translateMetadataDocument('view', …) → translateView → viewTranslationKey), which keys the catalog on view.name. With no name it threw. With the stamped name it served the container as ONE view keyed by the object: the name became the label fallback, and no view inside was translated. The config boot's 200 was the same fault without the crash.

The fix: packages/rest/src/meta-item-read-gate.ts only

translateMetaDocument (both transports call it: RestServer and the runtime dispatcher's /meta item read) classifies a view document once, with the spec's own isAggregatedViewContainer. A single view goes to the spec translator, unchanged. A container goes to translateMetaViewContainer, which translates it as a container, through translateView under each part's own key rule:

  • Each view inside it (list, form, each listViews / formViews entry) is translated as the item the composer expands it into. That is expandViewContainer, the same expansion both registrars run, so the view keeps its own name (OBJECT.KEY). Its packaged base is looked up by that qualified name. This is exactly what the by-name read of the expanded view is translated with, so the container read and the expanded read cannot disagree. The pin asserts that parity. The result lands back in the slot the view came from. The slot mapping follows the composer's documented order: a listViews entry is found by name (it is the first claim on its key, so it is never renamed). The default list / form is the item the composer flags isDefault, which covers a list that restates a named list and collapses into it. A formViews entry is found by position among the named forms, which covers one renamed by a list that took its key.
  • The container's own label / description are keyed by the object it binds (objects.OBJECT._views.OBJECT), with the packaged container as the base. That is the key the config boot already read for a container whose name is its object, and the key the i18n extractor scaffolds for such a container.
  • Nothing is invented. translateView falls back to a view's name for a missing label. That fallback is dropped (it is not a translation), which is what removes the config boot's label: "acme_note". With no catalog string, the SAME document reference comes back.
  • ⛔ Not view.name?.: that would leave a container untranslated in silence (triage 6094457162).

The binding comes from deriveViewContainerObject (@objectstack/metadata/view-container, the registry's one spelling, through its import-free leaf entry). It is not a fifth copy of the object → list.data.object → form.data.object → name chain. packages/spec is untouched, and so is packages/rest/src/rest-server.ts (PR #22628's region): the classification lives in the gate.

Packaging: @objectstack/metadata moves from devDependencies to dependencies in @objectstack/rest

What imports it at runtime, measured on the rebuilt packages/rest/dist/index.js at 7c71a9e34 (top-level ESM import … from lines):

  • @objectstack/metadata/view-container, 2 lines. One is this PR's import in meta-item-read-gate.ts. The other sits in the bundled ../objectql/dist/index.mjs section.
  • @objectstack/metadata/errors, 5 lines, and @objectstack/metadata/view-container-name, 2 lines. These are in the bundled ../metadata-protocol/src/protocol.ts and ../objectql/dist/index.mjs sections.

So the published package already loaded @objectstack/metadata at module load without declaring it. tsup bundles the dev dependency @objectstack/objectql, reached through rest-server.ts's optional await import('@objectstack/objectql'). That bundled code's own external imports of @objectstack/metadata/* are hoisted into rest's top-level imports. packages/objectql/dist/index.mjs carries the same three specifiers (/errors ×2, /view-container ×1, /view-container-name ×1). The manifest graph does not change (the edge existed as a dev edge), and check:workspace-manifest-cycles and check:undeclared-dep-imports both pass.

Tests

All at 7c71a9e34 (this branch merged with origin/main ee3ae0360), unless a line says otherwise.

  • Function pin, packages/rest/src/meta-view-container-i18n.test.ts: 10 passed. It covers both registrar shapes translating alike, CONTROL parity with the expanded by-name reads, nothing invented, the same reference with no applicable string or no locale, a collapsed default list, a renamed default list (default_2), a renamed named form (edit_2) beside the default form, bulk-action defs in their slot, and the per-view packaged base looked up by qualified name (an explicit override beats the catalog).
  • Boot pin, packages/cli/test/serve-view-container-read.integration.test.ts (integration tier: it spawns os serve; tier partition pin 22/22): one fixture, a real config boot plus os build and a real artifact boot. 6 passed. It covers each boot answering 200 with both views translated for zh-CN and no fabricated label, authored labels for en, the same container from both boots, and CONTROL expanded reads unchanged on both. Before the fix (rest dist built at 3d0eeefa4): 4 failed, 2 passed. The artifact boot answered 500, the config boot served All Notes, and the two containers differed. The two CONTROL / en config cases stayed green.
  • Ablation, committed fix at f63e6a1d1. The classification's if (metaType === 'view') was mutated through scripts/ablation-replace.mjs (anchor 1 → 0, blob fa91bb79 → cf670c38).
    • Function pin on mutated src: 10 of 10 failed.
    • pnpm --filter @objectstack/rest build, then ablation-dist-preflight reported the marker present in 2 built files. Boot pin: 4 failed, 2 passed.
    • Restore: blob back to fa91bb79 (equal to HEAD), git diff HEAD empty. Rebuild, then the --absent preflight passed: marker absent from all 6 built files, tree clean. Boot pin: 6 passed.
    • A first attempt at the dist leg is void: its rebuild never got the verify lock (exit 99) and its preflight reported the marker absent. It was stopped and restored, and its run is not counted.
  • pnpm --filter @objectstack/rest typecheck: exit 0 (including check:test-typecheck, 0 debt).
  • @objectstack/rest full suite (vitest run, both projects), at f63e6a1d1: 275 files, 5180 passed, 327 skipped, exit 0. The merge brought nothing under packages/rest, packages/metadata or packages/metadata-protocol. The incoming objectql/engine.ts change is file-field hydration. So at 7c71a9e34 the pins, the typecheck and the runtime consumers below were re-run, not the full suite.
  • Runtime dispatcher consumers of translateMetaDocument (packages/runtime/src/domains/meta-item-read-gate-parity, meta-item-envelope, meta-list-projection-parity, meta-read-org-scope-parity, meta-package-all-fold): 5 files, 793 passed.
  • Gates: dispatch-gates --commands at 7c71a9e34 derived 76 families. All 76 were run, and every exit code is 0. --ran reconciliation: 76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN. Two families needed a second run:
    • check:dual-build-cjs-loads: the first run reported PREREQUISITE NOT MET (9 unbuilt dists). The rerun passed (107 require entries across 66 packages load).
    • check:query-options-erasure: its self-test passed, but the first run's 420 s cap cut the ratchet. The rerun passed in 406 s.
  • Lint, a proven narrowing (CI runs the full pnpm lint), at 7c71a9e34: eslint --no-inline-config --format json packages/rest/src packages/cli/test/serve-view-container-read.integration.test.ts exited 0. It linted 303 files, ignored 0, with 0 errors and 0 warnings, and all three touched .ts files are in the linted set.
    • Population: eslint.config.mjs's own files globs (packages/**/*.{ts,tsx,mts,cts}, **/*.{ts,…}). No file reported as ignored.
    • Invariance: the config enables no type-aware linting. All 8 parserOptions are { ecmaVersion, sourceType }, with no project. Its only cross-file reads are its own two baselines, which this diff does not touch. So no untouched file's verdict can move.

Acceptance notes

  • Known limit. A container saved at runtime on ANOTHER package's object is expanded by the metadata protocol under its own name (OBJECT.NAME.KEY, expandUnderOwnName), which the gate does not reproduce. Its views are translated under the composer's names. This is stated in the function's docblock.
  • The list read is untouched. getMetaItems filters containers out of every view list (protocol.ts, isAggregatedViewContainer), so no container reaches translateMetaList. Measured: GET /meta/view?object=acme_note answered 200 with only the expanded items on both boots, at base.
  • The pin's home. No artifact-vs-config parity file exists on main yet. PR fix(metadata,runtime): a multi-package config boot registers its unowned top level under manifest.id, as the artifact boot does #22612's serve-config-boot-residual.integration.test.ts is in flight, so this is a sibling file built on the same helpers.
  • PR fix(metadata,runtime): a multi-package config boot registers its unowned top level under manifest.id, as the artifact boot does #22612. It changes how a MULTI-package config boot registers its unowned top level, through MetadataPlugin.registerUnclaimedTopLevel, which registers residual containers as shipped (no name). It reads only the list, so its read of such a container's by-name door is not covered there. This fix covers that shape too, since the container is classified by shape and not by name. This fixture is single-package (no packages[]), and fix(metadata,runtime): a multi-package config boot registers its unowned top level under manifest.id, as the artifact boot does #22612's own CONTROL pins a single-package config as unchanged. I checked this by reading its diff, not by a merged run (NOT MEASURED: a boot of this pin on a merge of fix(metadata,runtime): a multi-package config boot registers its unowned top level under manifest.id, as the artifact boot does #22612's head).
  • Observation, not filed (carrier: none). Rest's dist bundles its dev dependency @objectstack/objectql (and metadata-protocol sections), about 107k lines. It is reached through the try { await import('@objectstack/objectql') } catch { … } in rest-server.ts, whose comment says a deployment without the data engine "must degrade rather than fail to load". With the bundle, that import never fails, and a second copy of the objectql module ships inside rest. No wrong answer was reproduced.
  • Observation, not filed (carrier: none). i18n-extract.ts's top-level view walk sends a container that carries its own name down the single-view branch. It scaffolds _views.NAME and none of its views' keys, so a named container's _views.default / listViews keys are never scaffolded. Not exercised through a public door here.

Generated by Claude Code

…ntainer

The /meta item read handed a defineView container to the single-view
translator, which keys the catalog on the view's name. An artifact-shipped
container has none, so the artifact boot answered 500; the config boot's
registrar stamps one, so that boot served a fabricated label and left every
view inside untranslated. The read now classifies the served view document
once: a container's views are translated under the names the composer gives
them, with their own packaged bases, and land back in their slots.

Claude-Session: https://claude.ai/code/session_01HGa2XMKBRUqPrZFxQavBwH
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 8 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/rest/package.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 243dd3c6256673dc1a56f9419be03c397129f825.

⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/rest/package.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 17 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 243dd3c6256673dc1a56f9419be03c397129f825 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6519045629b06e0621c86e263c05fcff9fe00480 — the merge of head 7c71a9e34a9352727cbb09a9ded59deaa6f3bf59 into base 243dd3c6256673dc1a56f9419be03c397129f825, 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 6519045629b06e0621c86e263c05fcff9fe00480 && git checkout 6519045629b06e0621c86e263c05fcff9fe00480
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 243dd3c6256673dc1a56f9419be03c397129f825 7c71a9e34a9352727cbb09a9ded59deaa6f3bf59 && git checkout -B drift-repro 243dd3c6256673dc1a56f9419be03c397129f825 && git merge --no-ff 7c71a9e34a9352727cbb09a9ded59deaa6f3bf59

node scripts/docs-audit/affected-docs.mjs --json 243dd3c6256673dc1a56f9419be03c397129f825

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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 14:14
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 14:14
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit 4b7c630 Oct 10, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22614-meta-view-container-read branch October 10, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants