Commit 5555047
Fixes #21412
Clause-②: yes (narrowing)
The runtime save door (`saveMetaItem`, which REST `PUT
/api/v1/meta/view/:name` and the dispatcher's metadata save both call)
now refuses an aggregated view container whose body `name` disagrees
with the name it is saved under. It answers `VALIDATION_ERROR` / 400
before anything is stored or registered, and it refuses through the same
judge the source registrars call. Before this change, the card's probe
(row `crm_lead`, body `name` `lead_views`) was accepted, stored under
`crm_lead`, and registered under `lead_views` plus `crm_lead.default`,
so one document answered under two names.
This round follows the seat's answer on the card (comment 5961930912) to
the dev's `needs_decision` report (5961864645): Q1 A, Q2 A, Q3 A and Q4
A.
## What changed
- **One judge, in `@objectstack/metadata`.** New subpath
`@objectstack/metadata/view-container-name`
(`packages/metadata/src/view-container-name.ts`). The judgement: a
container's own `name`, when set, equals the key the door files the
container under. It has two entries, which share one gate, one envelope
and one message template:
- `viewContainerNameRefusal(container, sourceLabel, ownerId)` is the
source registrars' entry. Its key is DERIVED from the binding
(`deriveViewContainerObject`). It is the function that used to live in
`packages/objectql`, moved, and its words are byte for byte the same
(proof below).
- `savedViewContainerNameRefusal(container, saveName)` is the save
door's entry. Its key is the save name.
- **Why the save door's key is the save name, not the binding.** The
door keeps a container saved under a name other than its object (the
#13407 pin) and expands one on another package's object under its own
name (#21334's ruled arm). The dev's probes measured that the derived
key would refuse the body the door itself stores for that shape when it
is sent back, and would pass two bodies whose `name` disagrees with the
row (P3, P4).
- **Why `@objectstack/metadata`.** `@objectstack/core` cannot host the
judge: the judge needs `deriveViewContainerObject`, which lives in
`@objectstack/metadata`, and `@objectstack/metadata` lists core.
`@objectstack/metadata` is the one layer all three doors already depend
on. The judge is a subpath of its own, not the `./view-container` leaf,
because that leaf imports nothing and the judge needs
`isAggregatedViewContainer` from `@objectstack/spec`. It is not on the
root entry either, because the root loads the manager and the filesystem
machinery that objectql's ADR-0076 lean entry must not reach.
`check:lean-entry-closure` holds: `@objectstack/objectql/core` is 15
packages, the admitted set held exactly.
- **The save door** (`packages/metadata-protocol/src/protocol.ts`,
`saveMetaItem`) calls `savedViewContainerNameRefusal(request.item,
request.name)` for `view` first, before `normalizeViewMetadata` can keep
an authored `name`. Containers only. `normalizeViewMetadata`'s docblock
says so.
- **The artifact/HMR door's container branch**
(`packages/metadata/src/plugin.ts`) calls
`viewContainerNameRefusal(item, 'artifact', packageId)` after it derives
the key and before `memLoader.save` / `manager.register`. The probe
document is now refused through the judge, in the judge's words. Row 1
(`assertMetadataRegisterContract`) is unchanged for every type; on this
shape it is simply no longer reached.
- **`@objectstack/objectql`** keeps `viewContainerNameRefusal` and the
`ViewContainerNameRefusal` type as a re-export
(`src/view-container-name-refusal.ts`). Its module header is rewritten:
the judge's home, why it moved, why the source registrars' entry derives
the key and the save door's takes it. `engine.ts` and `packages/cli` are
untouched.
- **Rider 5954896314** (comment only): the comment above `ViewSchema`'s
`guidance:` in `packages/spec/src/ui/view.zod.ts` no longer says
`saveMetaItem` sends the name, artifact-shipped containers do, and the
sweep injects it. It says the door's own stamp (`normalizeViewMetadata`)
is the only platform writer of the key and states the one rule, worded
to Q1 A. No schema change.
## The message: one template, two renderings
The per-door words are one value, the door's key origin. It fills four
slots: the subject, the key clause, the text after the key, and the
cross-reference after the shared reason. Everything else is shared.
- The source registrars' rendering is byte-identical to the old objectql
function. This was proven by a temporary test that compared old and new
outputs over 10 fixtures, 3 of them refusals: message, `code`, `status`
and `httpStatus` were all equal, `Tests 1 passed`. The test was deleted
afterwards.
- The save door's rendering differs in three places, and each has a
reason:
- Its subject is `view container` and not `` `views:` container from
SOURCE 'OWNER' ``: the save door has no `views:` collection and no
owning manifest.
- Its key clause is `the name it is saved under`.
- It drops the cross-reference `the artifact/HMR loader refuses this
same document`, which would be false at this door for P3 and P4: the
artifact loader accepts a body whose `name` equals its binding.
- The ruling said the renderings differ "only in the clause that names
where the key came from". Read literally, that is one slot. All four
slots carry that one fact, so this is reported as a measured reading,
not chosen silently.
## Pins (triage pins, restated to Q1 A)
-
`packages/metadata-protocol/src/view-container-runtime-expansion.test.ts`,
the filing's own stub engine:
- P1, the card's probe, is refused `VALIDATION_ERROR` / 400. No row is
stored and nothing is registered.
- P1's message is exactly `savedViewContainerNameRefusal`'s, so the
refusal goes through the judge.
- P1's `code` / `status` equal the artifact/HMR registrar's for the same
document.
- P3 and P4 are refused, with nothing stored or registered.
- P2 (name equal to the row, bound elsewhere) passes. The container is
registered under exactly one key, its row key (Q4 A), and the object
door serves `crm_lead.default`.
- P2b (no name) passes, is stamped with the row name, and the stamped
body passes when it is read and sent back.
- A control with name, row and binding all equal passes, under one key.
- `packages/metadata/src/view-container-name.test.ts` covers:
- each entry on P1 to P4;
- the shapes where the two entries part;
- the boot precondition: a falsy derived key refuses nothing;
- the scope: a standalone ViewItem and a non-container are not judged
(#21470 remains open for the every-type half);
- the artifact door's container branch: it throws exactly the derived
entry's refusal and files nothing, and as a control, an agreeing or
absent `name` still registers.
- The boot loop's and `os validate`'s refusal words are unchanged, and
their pins are unedited:
`packages/objectql/src/view-container-name-refusal.test.ts` (green) and
`packages/cli/test/validate-view-container-name.test.ts` (CI; see
Tests).
- Edited because the ruling moves them: in
`packages/objectql/src/view-container-divergent-name-registrars.test.ts`,
the artifact door's two message assertions now read the judge's words
(`binds to, 'crm_lead'`, `` `name` is 'lead_views' ``) instead of row
1's. The envelope-equality pin is unchanged.
## Reverse verification (both from committed HEAD, through
`scripts/ablation-replace.mjs`)
- **Save door.** The call `if (nameRefusal) throw nameRefusal;` in
`protocol.ts` was disabled (anchor 1 to 0, blob `478daa416f90` to
`6f9ae10ffb7b`), and `view-container-runtime-expansion.test.ts` went `5
failed | 65 passed (70)`: P1 three times, P3 and P4. P2, P2b and the
control stayed green. The tool then restored the file to HEAD (`blob ==
HEAD (478daa4)`, `git diff HEAD` empty). Both test and subject
resolve the protocol from `src`, so no build was involved.
- **Artifact door.** The same call in `plugin.ts` was disabled (blob
`6924156b5fda` to `cef5ec76af37`), and `view-container-name.test.ts`
went `1 failed | 10 passed (11)`. The refusal came from row 1 instead
(no `httpStatus`), so the assertion failed. The file was restored to
HEAD. The test imports `./plugin.js` from `src`.
## Tests
Code at `a70d0d61a4` is identical to `7d4ee0ac46` outside `.changeset/`.
All runs went through `scripts/pm/os-verify-lock.sh`, and each gave
`VERDICT command-exit 0`:
- `@objectstack/metadata`: `pnpm test` gave `Test Files 57 passed (57)`,
`Tests 847 passed (847)`, and `pnpm typecheck` exited 0. Its tsconfig
includes `src/**/*`, so the tests are type-checked.
- `@objectstack/metadata-protocol`: `pnpm test` gave `Test Files 205
passed | 3 skipped (208)`, `Tests 3092 passed | 19 skipped (3111)`, and
`pnpm typecheck` exited 0 (tests included).
- `@objectstack/objectql`: the `local` project gave `Test Files 366
passed (366)`, `Tests 7383 passed (7383)`, `test:repo` gave `1 passed`,
and `typecheck` exited 0. That covers `tsc`, the scripts project, and
`check:test-typecheck` OK, held in the debt ledger.
- Build: `turbo run build --filter=@objectstack/objectql^...
--filter=@objectstack/objectql` gave `14 successful`. The new subpath
loads under both conditions (`require` and `import` each return both
entries, `VALIDATION_ERROR` 400), and `dist/view-container-name.d.ts` /
`.d.cts` are emitted.
- **Not run here: `packages/cli`**, the `os validate` pins.
`@objectstack/objectql`'s export keeps its name, signature and bytes of
output, and the cli imports it unchanged. Building the cli closure is 60
tasks, 11 of them cached. CI runs them. The byte-identity proof above is
the local evidence.
- One count control moved:
`packages/metadata/src/serializers/typescript-serializer-annotation.test.ts`
pins how many `exports` entries it visits (5 to 6), so the new entry is
checked too.
## Gates
`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` was derived at `a70d0d61a4` and gave 99 commands. All 99
were run at `a70d0d61a4`:
- 98 exited 0.
- 1 is **NOT MEASURED**: `pnpm check:dual-build-cjs-loads` exited 3 with
`PREREQUISITE NOT MET`, because it needs every package's `dist/` (the
direct load check above stands in, but is not the gate).
- Of note: `check-adr-0087-registration --base origin/main`: `1
declared-breaking changeset(s), each carrying an ADR-0087 disposition`.
`check-changeset-no-major`: no major; the level axis needs a PR payload.
`check:lean-entry-closure`: admitted set held. `check:published-files`,
`check:dts-closure`, `check:issue-citations` (22 resolve),
`check:doc-authoring`, `check:spec-docblock-symbol-anchors`,
`check:nul-bytes`, `check:test-source-alias`,
`check:cross-package-test-inputs`: all green.
Lint, a proven narrowing at `a70d0d61a4`: `eslint --no-inline-config
--format json` over the 10 touched `.ts` files reports 10 files, 0
errors, 0 warnings, and none ignored. Every one is in eslint's
population (`--print-config` resolves each). `eslint.config.mjs` never
enables type-aware linting (its own note near line 327: no
`parserOptions.project`, no typed rules), so this diff cannot move the
verdict on any untouched file. The repo-wide `pnpm lint` is CI's.
## Changesets
- `@objectstack/metadata` `minor`: the new subpath, and the artifact
door's refusal speaking through the judge.
- `@objectstack/metadata-protocol` `minor` with a **BREAKING** banner:
an accept-set narrowing at the save door, graded like the boot loop's
refusal of the same divergence. Its ADR-0087 disposition is
`not-required (no-migration-prescription)`.
- `@objectstack/objectql` `patch`: the re-export.
- `@objectstack/spec` `patch`: comment only. `src/**/*.zod.ts` ships as
source, and the comment ships in the `ui` JavaScript output (measured:
the new sentence is in 20 `dist` files; the old one is in none).
**The `Clause-②` line above is the claim's, copied as dispatched.** By
`scripts/pm/clause2-line.mjs`'s own definitions, this diff both widens a
public surface (the new `@objectstack/metadata` subpath, with two
functions and a type) and narrows an accept set (the save door). That
reads as `yes (narrowing)`. This is raised to the seat in the dev
report; the line here is not changed by the dev.
## Acceptance notes
- `carrier: #21470` (#21470 remains open). Container bodies stored
before this change that already carry a divergent `name` keep
registering under that `name` at boot (`loadMetaFromDb`, then
`hydrateOverlayIntoRegistry`). `revertCommit` and `rollbackMetaItem`
re-persist stored versions through `repo.restoreVersion` without passing
the save seam. The count of such rows is not measured.
- The every-type half (a record view or any other type saved with a body
`name` that differs from the row) is #21470's, and is not judged here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent ab52182 commit 5555047
15 files changed
Lines changed: 631 additions & 118 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- metadata
- src
- serializers
- objectql/src
- spec/src/ui
Lines changed: 19 additions & 0 deletions
| 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 | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
97 | 101 | | |
98 | 102 | | |
99 | 103 | | |
| |||
1218 | 1222 | | |
1219 | 1223 | | |
1220 | 1224 | | |
1221 | | - | |
| 1225 | + | |
| 1226 | + | |
| 1227 | + | |
| 1228 | + | |
1222 | 1229 | | |
1223 | 1230 | | |
1224 | 1231 | | |
| |||
17713 | 17720 | | |
17714 | 17721 | | |
17715 | 17722 | | |
| 17723 | + | |
| 17724 | + | |
| 17725 | + | |
| 17726 | + | |
| 17727 | + | |
| 17728 | + | |
| 17729 | + | |
| 17730 | + | |
| 17731 | + | |
| 17732 | + | |
| 17733 | + | |
| 17734 | + | |
| 17735 | + | |
| 17736 | + | |
| 17737 | + | |
| 17738 | + | |
17716 | 17739 | | |
17717 | 17740 | | |
17718 | 17741 | | |
| |||
Lines changed: 122 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
| |||
773 | 775 | | |
774 | 776 | | |
775 | 777 | | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
59 | 69 | | |
60 | 70 | | |
61 | 71 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
229 | 229 | | |
230 | 230 | | |
231 | 231 | | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
232 | 236 | | |
233 | 237 | | |
234 | 238 | | |
| |||
1177 | 1181 | | |
1178 | 1182 | | |
1179 | 1183 | | |
| 1184 | + | |
| 1185 | + | |
| 1186 | + | |
| 1187 | + | |
| 1188 | + | |
| 1189 | + | |
| 1190 | + | |
| 1191 | + | |
| 1192 | + | |
| 1193 | + | |
1180 | 1194 | | |
1181 | 1195 | | |
1182 | 1196 | | |
| |||
Lines changed: 3 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
171 | | - | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
172 | 174 | | |
173 | 175 | | |
174 | 176 | | |
| |||
0 commit comments