Repository navigation
Commit e367002
Fixes #21558
Clause-②: yes (narrowing) — the runtime save door's accept set narrows:
a view container saved under a name its own expansion produces is
refused with `VALIDATION_ERROR` / 400. Nothing widens.
## What changed
`saveMetaItem` (`packages/metadata-protocol/src/protocol.ts`), the
method behind `PUT /api/v1/meta/view/NAME` and the dispatcher's metadata
save, now refuses a view container saved under a name its own expansion
produces. The card's case is `{ name: 'showcase_task.default', object:
'showcase_task', list }` saved as `showcase_task.default`, the name its
bare `list` expands to. This implements triage's ruling 5966930701:
refuse at the write door, with a named error and the prescription. ⛔ The
read doors are not changed, and no stored row is re-saved.
- **Where.** A new private method, `containerOwnExpansionNameRefusal`,
is called right after the name check placed at this door earlier
(`savedItemNameRefusal`). It runs before the view identity stamp
(`normalizeViewMetadata`).
- **The predicate.** It asks the readers' own expansion,
`expandRuntimeViewContainer`, whether the save name is one the container
expands to. It does not copy the naming rule. So every member kind (a
bare or named `list`, `listViews`, `form`, `formViews`) and the
expander's de-duplicated names (`_2`) are judged where the readers place
them. A container on another package's object expands under its own name
(the arm from #21334) and is never refused. A body with no `name` is
judged under the save name the door stamps on it.
- **The envelope.** `VALIDATION_ERROR` / 400, the same as the name check
it sits beside. H4: the existing save-door name refusal uses this code,
so no new code is minted and the error-code ledger is untouched.
- **The words.** "Invalid view container: it is saved under 'NAME',
which is a name its own expansion produces (its list view on 'OBJECT').
An expanded view fills only a name that has no stored row of its own,
and this container would be that row, so no read would answer a view
under 'NAME'. Save the container under its object's name, 'OBJECT', or
save a view item (name, object, viewKind and config) under 'NAME'." The
text carries no tracker number.
### Every accept-set change at `saveMetaItem`, type `view`
| Input | Before | After |
|---|---|---|
| A container whose save name is one of its own expansion's names.
Applies to publish and draft mode, both scopes and both kernels. |
Accepted: stored, and registered on an unscoped kernel. | Refused
`VALIDATION_ERROR` / 400. Nothing is stored or registered. |
| The same container with no body `name` (the door stamps the save
name). | Accepted. | Refused, with the same envelope. |
| A container whose only member is `form`, saved under its own expanded
name where the registry already holds a view item of that name. |
Refused `INVALID_METADATA` / 422. The identity stamp copied that item's
`viewKind` onto the body, so the schema saw a malformed view item. |
Refused `VALIDATION_ERROR` / 400 by this check, which now runs first. |
| Everything else. | Unchanged. | Unchanged. |
### What still saves (the ruling's controls, pinned on both kernels and
in both scopes)
- A container under its object's name saves and expands as before.
- A view item under an expanded name saves, as #21510's sanctioned
override for that name.
### Stored rows and the other writers through this door
- The read doors are unchanged. A row stored in this shape before this
change keeps its bytes and reads as it does after #21510: the by-name
read answers the raw container, and the object door lists nothing under
that name. The container's other expanded names still fill names that
have no row of their own. Delete stays open. Re-saving the same body is
refused, with the prescription.
- `migrateStoredMetadata` (`os migrate meta --stored`) and
`duplicatePackage` re-save stored rows through this door. For such a row
they now record the refusal: migration as a `failed` row with this
reason, duplication as a `failed[]` entry. Neither re-saves it.
## Census (taken before the refusal was written)
At BASE `37442d4750`, objectui at its pin `89cad75d55`:
- **Writers in this repository.** The view writers that reach
`saveMetaItem` are REST `PUT /api/v1/meta/view/NAME` and the dispatcher
(`runtime/src/domains/meta.ts:1424`), which pass the caller's body
through, plus `migrateStoredMetadata` and `duplicatePackage`.
`runtime/src/domains/packages.ts:1583` saves `app` only, and
`runtime/src/domains/automation.ts:1628` saves `flow` only.
- **Studio (objectui at its pin).** No Studio writer produces the shape
by default:
- `app-shell` `ObjectView.tsx:1471` saves through
`buildViewConfigSaveBody`, and `:1530` through `viewEnvelope`.
`ObjectDataPage.tsx:381` uses `createRuntimeMetadata`. All three write a
view item (`viewKind: 'list'`).
- `data-objectstack` `index.ts:5522` (`setViewConfig`) and `:5811`
(`createView`) write flat view configs. `updateView` reduces a container
it reads to its `list` (`:5895`).
- `PublicFormsPage.tsx:229/301` saves items listed by `getMetaItems`,
which never lists a container.
- The metadata-admin `createBuildBody` (`anchors.ts:291`) emits a view
item.
- The metadata-admin edit page (`ResourceEditPage.tsx:1477`) saves a
body under its own `name`. It produces the refused shape only if an
author types an expanded name into a container's `name`.
- **AI author.** This repository has no view-writing AI tool.
`service-ai` was removed in `21d4f8901b` (the open edition is MCP-only,
ADR-0025 S2), and the MCP tool list in `mcp-http-tools.ts` has no
metadata write. The published `skills/objectstack-ui` tells authors to
write `defineView` containers in source. Those go through the source
registrars, which refuse a container `name` that disagrees with its
object. The cloud AI author is outside this repository: NOT MEASURED.
- **Packaged containers.** There are 12 `defineView(` sites in
`examples/` (crm 3, showcase 7, todo 2), and none carries a top-level
`name`. `platform-objects` carries object-level `listViews`, not view
containers. Structurally, a source registrar files a container under its
object and refuses a `name` that disagrees with it, and an expanded name
(OBJECT.KEY) is never the object's name. So a packaged container under
its own expanded name cannot boot.
- **Stored rows.** The example apps seed no `sys_metadata` view rows. In
this repository's tests, no suite stores this shape: the full
`metadata-protocol` suite and the downstream samples below stay green
with the refusal on. Hosted tenants: NOT MEASURED.
## Tests
- **Premise, measured on this branch** before the fix (BASE
`37442d4750`, pins present, refusal absent). `vitest run
src/view-container-runtime-expansion.test.ts -t '#21558'` gave **27
failed / 9 passed**:
- 23 refusal pins failed with `expected null to be an instance of
Error`, meaning the save was accepted. These are 16 member cells, 4
draft cells and 3 runtime-object cells.
- The 4 `form` cells answered `INVALID_METADATA` / 422 (the
identity-stamp row in the table above).
- The 9 controls passed.
- **Door probe** (a throwaway test, deleted afterwards), with the
refusal ablated on both kernels: `save=accepted objectDoor=[] byName=raw
container`. With the fix: `save=refused VALIDATION_ERROR/400` on both
kernels.
- **With the fix, at `079069661f`:**
- The file: 181 passed.
- `pnpm --filter @objectstack/metadata-protocol exec vitest run
--maxWorkers=2`: Test Files 207 passed | 3 skipped (210), Tests 3248
passed | 19 skipped (3267), `VERDICT command-exit 0`.
- `typecheck` (`tsc --noEmit`): `VERDICT command-exit 0`. `tsc
--listFiles` includes the test file.
- **New pins**, 37 in all, in
`view-container-runtime-expansion.test.ts`:
- On both kernels × both scopes: every member kind saved under its own
expanded name is refused with the envelope, nothing is stored or
registered, and every packaged name still answers its packaged view on
both doors. The card's save is refused in draft mode too.
- The two ruled controls.
- On a runtime-authored object: `crm_lead.default`, `crm_lead.pipeline`,
the de-duplicated `crm_lead.default_2`, and an unnamed container, plus a
control.
- **Downstream sample**, against the rebuilt dist. Direction: consumers
of `@objectstack/metadata-protocol`, built with `turbo run build
--filter='@objectstack/metadata-protocol...'
--filter='@objectstack/objectql^...' --filter='@objectstack/rest^...'`,
24 tasks, `VERDICT command-exit 0`:
- objectql: `protocol-meta`, `protocol-view-identity-overlay`,
`protocol-org-overlay-registry-gate` and `protocol-commit-history` (4
files, 162 tests), plus `metadata-validation-sweep`,
`view-container-divergent-name-registrars` and
`engine-nested-plugin-view-expansion` (3 files, 27 tests).
- rest: `public-form-routes.stored-row` (1 file, 7 tests).
- All pass. The rest of the downstream run is CI's.
## Reverse verification
The fix was committed first (`df4fcd4636`). A trap-guarded script then
ran `scripts/ablation-replace.mjs` on the anchor `if
(ownExpansionRefusal) throw ownExpansionRefusal;`:
- **Mutation.** The anchor went from 1 occurrence to 0 and the blob from
`19953a79f484` to `96e1e0adefea`.
- **Result.** `-t '#21558|PROBE'` gave **28 failed / 10 passed**: every
refusal pin went red, and the 9 controls plus the probe stayed green.
The direction is red, as predicted.
- **Restore.** `git checkout HEAD -- ABS_PATH` brought the blob back to
`19953a79f484`, equal to HEAD, and `git diff HEAD` was empty. Both the
tool and the script's own trap verified this.
- **No dist leg.** The subject is imported through the relative
`./index.js` (the source), not through a package `exports`.
## Gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths) at `079069661f` derived **64**
families: the dispatch lead's 50, plus the ones this changeset and the
test added.
- All 64 exited 0.
- `check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT MET:
dists missing). After a workspace build it measured 106 entries in 66
packages and exited 0.
- `--ran`: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.
- `check:adr-0087-registration` accepts the changeset's `not-required
(no-migration-prescription)` disposition.
- Lint, narrowed: `eslint --no-inline-config --format json` over the 2
changed `.ts` files reports 2 files, 0 errors, 0 warnings. The changeset
`.md` has no matching ESLint configuration. Type-aware linting is never
enabled (`eslint.config.mjs:326-328`, and `--print-config` shows no
`parserOptions.project`), so files this diff does not touch cannot
change verdict. Repo-wide `pnpm lint` is CI's.
- Over REST (`PUT /api/v1/meta/view/NAME` on a booted stack): NOT
MEASURED. The pins are in-process at the method that door calls, on both
kernels.
## Acceptance notes
- **A sibling shape stays open (reported to the seat, not fixed here).**
A container saved under a name that ANOTHER container's expansion
produces is accepted. Measured in-process on both kernels: a container
`crm_lead` with `listViews.pipeline` is stored, then `{ name:
'crm_lead.pipeline', object: 'crm_lead', list }` is saved as
`crm_lead.pipeline`. The save is accepted. The object door then lists
nothing under `crm_lead.pipeline`, and its `crm_lead.default` becomes
the second container's list. The by-name read answers the raw container.
The ruling covers only a name the container's own expansion produces.
- **The view identity stamp's container test omits `form`.**
`viewIdentityPatch` leaves `list`, `listViews` and `formViews`
containers alone, but not a container whose only member is `form`. Saved
under the name of a registered view item, such a container takes that
item's `viewKind` and is refused 422 as a malformed view item. For its
own expanded names this check now answers first. Under any other view
item's name, that is the sibling shape above.
- **Restore and publish doors.** `rollbackMetaItem`, `revertCommit` and
the draft promotion do not run this check. A version or draft stored
before this change can still be written back in this shape. A new draft
in this shape can no longer be stored. Kept to the save door per the
claimed surface.
- **Package binding.** The expansion is judged with the request's
package binding, which is the binding the registry write-through
registers it under.
- `dist/index.d.ts` gains one private member line. No public member or
exported type changes.
#21510 and #21511 are context only; this PR leaves both as they are.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent f97660c commit e367002
3 files changed
Lines changed: 257 additions & 0 deletions
File tree
- .changeset
- packages/metadata-protocol/src
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17884 | 17884 | | |
17885 | 17885 | | |
17886 | 17886 | | |
| 17887 | + | |
| 17888 | + | |
| 17889 | + | |
| 17890 | + | |
| 17891 | + | |
| 17892 | + | |
| 17893 | + | |
| 17894 | + | |
| 17895 | + | |
| 17896 | + | |
| 17897 | + | |
| 17898 | + | |
| 17899 | + | |
| 17900 | + | |
| 17901 | + | |
| 17902 | + | |
| 17903 | + | |
| 17904 | + | |
| 17905 | + | |
| 17906 | + | |
| 17907 | + | |
| 17908 | + | |
| 17909 | + | |
| 17910 | + | |
| 17911 | + | |
| 17912 | + | |
| 17913 | + | |
| 17914 | + | |
| 17915 | + | |
| 17916 | + | |
| 17917 | + | |
| 17918 | + | |
| 17919 | + | |
| 17920 | + | |
| 17921 | + | |
| 17922 | + | |
| 17923 | + | |
| 17924 | + | |
| 17925 | + | |
| 17926 | + | |
| 17927 | + | |
| 17928 | + | |
| 17929 | + | |
| 17930 | + | |
| 17931 | + | |
| 17932 | + | |
| 17933 | + | |
| 17934 | + | |
| 17935 | + | |
| 17936 | + | |
| 17937 | + | |
| 17938 | + | |
| 17939 | + | |
| 17940 | + | |
| 17941 | + | |
| 17942 | + | |
| 17943 | + | |
| 17944 | + | |
| 17945 | + | |
| 17946 | + | |
| 17947 | + | |
| 17948 | + | |
| 17949 | + | |
| 17950 | + | |
| 17951 | + | |
| 17952 | + | |
| 17953 | + | |
| 17954 | + | |
| 17955 | + | |
| 17956 | + | |
| 17957 | + | |
17887 | 17958 | | |
17888 | 17959 | | |
17889 | 17960 | | |
| |||
18365 | 18436 | | |
18366 | 18437 | | |
18367 | 18438 | | |
| 18439 | + | |
| 18440 | + | |
| 18441 | + | |
| 18442 | + | |
| 18443 | + | |
| 18444 | + | |
| 18445 | + | |
| 18446 | + | |
| 18447 | + | |
| 18448 | + | |
18368 | 18449 | | |
18369 | 18450 | | |
18370 | 18451 | | |
| |||
0 commit comments