Repository navigation
Commit aefab7c
fix(rest)!: the /meta item and list reads serve a view stored with no label without an invented label (#24013)
Fixes #23967
Clause-②: no (narrowing)
## What changes
`GET /api/v1/meta/view/NAME` and `GET /api/v1/meta/view` served a view
stored with no `label` as if it had one: `label` equal to its qualified
name (`OBJECT.KEY`). This happened on the standalone server and on the
runtime dispatcher. The spec declares a view's `label` optional with no
default (`packages/spec/src/ui/view.zod.ts`,
`I18nLabelSchema.optional()`). The value came from the read-side
translator: `translateView` answers `view.label ?? view.name` when the
catalog carries no string for the view. The view-container read already
dropped that fallback (#22614); the item and list reads did not.
The rule is now one module-private helper in
`packages/rest/src/meta-item-read-gate.ts`,
`withoutInventedViewLabel(input, translated)`. All three `view` sites
call it:
- `translateMetaDocument`'s single-view arm: the item read.
`translateMetaEnvelope` wraps it, and `RestServer.translateMetaItem`
delegates to it.
- `translateMetaList`'s per-element map: the list read.
`RestServer.translateMetaItems` delegates to it, and the runtime
dispatcher's `/meta` domain imports it.
- `projectViewTranslation`: container slots and the container's own
strings. Its inline check is replaced by the helper, with the same
semantics. The reverse run below shows the container answer is
unchanged.
The rule: a `label` equal to the view's `name`, where the stored view
carried none, is not served. An authored label is served as authored,
including one equal to the name. A catalog translation is still served,
for a view with a label and for one without. A catalog string equal to
the qualified name cannot be told apart from the fallback, so it is
dropped with it, as it already was on the container path. No export is
added. `packages/spec`, `rest-server.ts` and
`runtime/src/domains/meta.ts` are not touched.
The console half is a separate card that the seat files on
objectstack-ai/objectui when this lands: `viewTabLabel` (`view.label ||
view.name || view.id`) needs its own human fallback. It is not addressed
here, and the console tab keeps showing a machine name for such a view
until then.
## Evidence
### Live boot, BASE `adce19297` vs HEAD (standalone `os dev`)
From a scratch directory outside the repo: `examples/app-todo` compiled
with `os compile -o SCRATCH/objectstack.json`, booted with `os dev
--artifact ./objectstack.json --fresh -p PORT` and placeholder admin
credentials. Recipe: `POST /packages` (201), `PUT
/meta/object/recheck_gate?package=com.qa.recheck` with label, plural
label and `sharingModel` (200), `PUT /meta/view/recheck_gate.default`
with no `label` (200), `PUT /meta/view/recheck_gate.named` with `label:
"All gates"` (200).
| read (no Accept-Language, `en`, and `zh-CN`: the same in all three) |
BASE | HEAD |
| --- | --- | --- |
| `GET /meta/view/recheck_gate.default` | 200, `label:
"recheck_gate.default"` | 200, no `label` key |
| `GET /meta/view`, element `recheck_gate.default` | 200, `label:
"recheck_gate.default"` | 200, no `label` key |
| `GET /meta/view`, elements whose `label` equals `name` |
`[recheck_gate.default]` | `[]` |
| `GET /meta/view/recheck_gate.named` and its list element | 200, `"All
gates"` | 200, `"All gates"` |
| `GET /meta/view/recheck_gate.default/layers`, overlay and effective |
no `label` | no `label` |
### Cloud host shape, BASE vs HEAD
`new HttpDispatcher(kernel)`, which is what `createHonoApp` builds,
loaded from the built `@objectstack/runtime` dist. That dist imports the
built `@objectstack/rest`. The protocol double serves three stored
views: label-less `default`, authored `named`, and label-less `tagged`,
which the `zh-CN` catalog translates.
| case | BASE | HEAD |
| --- | --- | --- |
| default locale resolves, no Accept-Language: `default` / `tagged`,
item and list | `label` = own name | no `label` key |
| `zh-CN`: `default`, item and list | `label: "recheck_gate.default"` |
no `label` key |
| `zh-CN`: `tagged` / `named` | `已标记` / `全部闸门` | `已标记` / `全部闸门` |
| no default locale, or no i18n service, no Accept-Language | no `label`
key (nothing is translated) | no `label` key |
The last row is the PM's mechanism assumption 2: with no resolvable
locale, no other layer adds the label.
### Pins: `packages/rest/src/meta-view-label-not-invented.test.ts`, 39
rows
- §1 runs through the standalone routes `GET /api/v1/meta/:type/:name`
and `GET /api/v1/meta/:type`, over a real `SchemaRegistry` and the real
`ObjectStackProtocolImplementation`, with the views seeded as stored
rows. It has one row per locale and view: label-less (no key), authored,
catalog-translated label-less, and authored equal to name. It also
covers the card's read with no Accept-Language and no catalog.
- §2 runs through `translateMetaEnvelope` and `translateMetaList`, the
functions the dispatcher's `/meta` domain imports, with the same rows, a
bare array and the `{ type, items }` envelope. It also covers no
catalog, no mutation, and no locale (the same document is returned).
- §3 is a CONTROL: the container read answers what it answered before.
### Reverse verification and ablation (one-shot, nothing left in the
tree)
The subject is reached by relative import (`./meta-item-read-gate.js`,
and `./rest-server.js` imports it relatively), so it resolves to `src/`;
no rebuild sits between legs. Each leg below was proven on disk. The
reverse leg's blob equals the BASE blob. Each ablation went through
`scripts/ablation-replace.mjs`: anchor 1 to 0, blob changed. Each
restore was proven: blob equals the HEAD blob and `git diff HEAD` is
empty.
| leg | result |
| --- | --- |
| CONTROL, HEAD unmutated | 39 / 39 green |
| REVERSE, the whole file at BASE | 15 red, exactly the label-less rows
on both reads. All authored and translated rows and both §3 container
rows stay green, so the container answer is unchanged. |
| item site ablated (`return translated`) | 9 red, the item-side
label-less rows only |
| list site ablated (`return out`) | 9 red, the list-side label-less
rows only |
| container site ablated (helper bypassed in `projectViewTranslation`) |
the 2 §3 rows red |
### Suites
These ran post-merge at `6ff4da0d8`. The one later commit adds only the
ledger row below.
- `pnpm --filter @objectstack/rest typecheck`: exit 0. `tsc -p
tsconfig.test.json --listFiles` includes the new file, with 0 `error
TS`.
- `pnpm --filter @objectstack/rest test` (`local`): 288 files, 5941
passed, 326 skipped.
- `pnpm --filter @objectstack/rest test:repo` (`repo`): 5 files, 250
passed, 11 skipped.
- `@objectstack/runtime`, the 25 `src/domains/meta-*.test.ts` files of
the `local` project: 513 passed. The `repo` project's
`meta-list-projection-parity.test.ts`: 739 passed. Runtime aliases
`@objectstack/rest` to its source.
- `pnpm exec eslint --no-inline-config` over the two changed TS files:
exit 0.
### Gates, at the final HEAD `203a8192f`
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derives 72 commands from merge base
`d132140eb`.
- All 72 were run on `203a8192f`, each with its exit code captured
before any pipe, and all 72 exit 0.
- `--ran` reconciliation over that record reads: 72 derived, 72 run, 0
NOT-MEASURED, 0 UNRUN (a derived zero: all 72 recorded an exit code).
- Included: `check:adr-0087-registration` (the changeset holds
`not-required (no-migration-prescription)`), `check:changeset-no-major`,
`check:empty-changeset`, `check:engine-double-contract` (1017 pinned),
`check:test-source-alias`, `check:cross-package-test-inputs`,
`check:nul-bytes` and `check:dual-build-cjs-loads` (110 require entry
points across 68 packages load).
- The repo-wide lint and every family CI owns are read from this PR's
check-runs.
## Surface note
The claim's file surface was the source file, one test file and one
changeset. A fourth file is needed:
`scripts/engine-double-contract.pinned.json` gains one row
(`packages/rest/src/meta-view-label-not-invented.test.ts`, `findOne`,
pinned 1). `check:engine-double-contract` requires it for the new pin's
fake engine and prescribes `--write`. The regeneration printed 0 rows
lost; no other row moves.
## Acceptance notes
- **Other translatable types (PM assumption 5).** The `object`, `app`,
`dashboard`, `dataset`, `page` and `picklist` translators serve `label`
only from the catalog or the document; none falls back to `name`.
`action` does: `resolveActionLabel` answers `action.name` and
`translateAction` always writes `label`. But `ActionSchema` declares
`label` required (`packages/spec/src/ui/action.zod.ts:959`), so a
schema-valid stored action always carries one. No reach was measured, so
this is not filed.
- **The fallback's source (PM assumption 6).** `resolveViewLabel`'s
`view.label ?? view.name`
(`packages/spec/src/system/i18n-resolver.ts:481`) is unchanged.
- In this repo, `translateView` is called by this module's three sites
and by the public-form route in `rest-server.ts`. That route takes only
the translated `config` and keeps its own label reading.
- `packages/cli/src/utils/i18n-extract.ts` names both functions in
docblocks only, with no import.
- objectui main (`63d00da`) calls neither.
- After this PR, no served door carries the fallback. The exported
function's answer is `domain:spec` ground; it is noted and not filed.
- **Consumers (PM assumption 4).**
- The published response types are `GetMetaItemResponseSchema.item:
z.unknown()` and `GetMetaItemsResponseSchema.items:
z.array(z.unknown())`.
- No `packages/client` type requires a served view's `label`, and no
test asserts that a served view's `label` equals its name.
- The CLI's `serve-view-container-read` integration test asserts
authored labels.
- The dogfood `view-container-cross-package-default` test compares the
item and list reads, which now take the same rule.
- The card's object recipe now needs `sharingModel`. Without it the PUT
answers 422 `security-owd-unset`, an authoring rule that postdates the
card's run.
- Booting a compiled artifact from a directory with no `node_modules`
logged `runtime module load FAILED ... Cannot find package
'@objectstack/spec'`, and the todo app's hook was refused at boot. This
was seen only in this scratch setup, and it does not touch `/meta`.
Whether an artifact is meant to boot without its dependencies is not
established here; noted, not filed.
---
_Generated by [Claude
Code](https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent bb14934 commit aefab7c
4 files changed
Lines changed: 417 additions & 11 deletions
File tree
- .changeset
- packages/rest/src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2794 | 2794 | | |
2795 | 2795 | | |
2796 | 2796 | | |
| 2797 | + | |
| 2798 | + | |
| 2799 | + | |
| 2800 | + | |
2797 | 2801 | | |
2798 | 2802 | | |
2799 | 2803 | | |
| |||
2820 | 2824 | | |
2821 | 2825 | | |
2822 | 2826 | | |
2823 | | - | |
2824 | | - | |
2825 | | - | |
2826 | | - | |
| 2827 | + | |
| 2828 | + | |
| 2829 | + | |
| 2830 | + | |
| 2831 | + | |
| 2832 | + | |
| 2833 | + | |
2827 | 2834 | | |
2828 | 2835 | | |
2829 | 2836 | | |
| |||
2851 | 2858 | | |
2852 | 2859 | | |
2853 | 2860 | | |
| 2861 | + | |
| 2862 | + | |
| 2863 | + | |
| 2864 | + | |
2854 | 2865 | | |
2855 | 2866 | | |
2856 | 2867 | | |
| |||
2878 | 2889 | | |
2879 | 2890 | | |
2880 | 2891 | | |
2881 | | - | |
| 2892 | + | |
2882 | 2893 | | |
2883 | 2894 | | |
2884 | 2895 | | |
| 2896 | + | |
2885 | 2897 | | |
2886 | 2898 | | |
2887 | 2899 | | |
| |||
2912 | 2924 | | |
2913 | 2925 | | |
2914 | 2926 | | |
2915 | | - | |
2916 | | - | |
| 2927 | + | |
| 2928 | + | |
| 2929 | + | |
2917 | 2930 | | |
2918 | 2931 | | |
2919 | 2932 | | |
| |||
3004 | 3017 | | |
3005 | 3018 | | |
3006 | 3019 | | |
3007 | | - | |
3008 | | - | |
| 3020 | + | |
| 3021 | + | |
3009 | 3022 | | |
3010 | 3023 | | |
3011 | 3024 | | |
| |||
3018 | 3031 | | |
3019 | 3032 | | |
3020 | 3033 | | |
3021 | | - | |
| 3034 | + | |
3022 | 3035 | | |
3023 | | - | |
3024 | 3036 | | |
3025 | 3037 | | |
3026 | 3038 | | |
| |||
3033 | 3045 | | |
3034 | 3046 | | |
3035 | 3047 | | |
| 3048 | + | |
| 3049 | + | |
| 3050 | + | |
| 3051 | + | |
| 3052 | + | |
| 3053 | + | |
| 3054 | + | |
| 3055 | + | |
| 3056 | + | |
| 3057 | + | |
| 3058 | + | |
| 3059 | + | |
| 3060 | + | |
| 3061 | + | |
| 3062 | + | |
| 3063 | + | |
| 3064 | + | |
| 3065 | + | |
| 3066 | + | |
| 3067 | + | |
| 3068 | + | |
| 3069 | + | |
| 3070 | + | |
| 3071 | + | |
| 3072 | + | |
| 3073 | + | |
| 3074 | + | |
| 3075 | + | |
| 3076 | + | |
| 3077 | + | |
| 3078 | + | |
| 3079 | + | |
3036 | 3080 | | |
3037 | 3081 | | |
3038 | 3082 | | |
| |||
0 commit comments