Repository navigation
Commit db87a02
fix(metadata-protocol): another package's withdrawal of a form holds at the anonymous form endpoints, whatever packages' copies of a view container are saved (#21979)
Fixes #21967
Clause-②: no
Another package's withdrawal of a form holds at the anonymous form
endpoints, whatever packages' copies of a view container are saved. This
is triage's direction A (6012036101, unlocked by 6013043442): the view
list's expansion upsert is keyed per package. A package-bound container
row writes only its own package's slot, and a package-less row stands in
for every package with no row of its own (ADR-0048). The by-name read
selects through the same function, so the list and the by-name read
agree.
All source edits are in `@objectstack/metadata-protocol`
(`packages/metadata-protocol/src/protocol.ts`). The other edits are a
docblock in `@objectstack/metadata-core` and the done-when sentence on
`content/docs/ui/public-data-collection.mdx` (declared to `domain:devx`
on #6023). No `rest-server.ts` edit, no `packages/spec` edit, no
governed path.
## Commits (each can be read on its own)
1. `2981f6eb78` test: the pins below, committed red against the base
`protocol.ts`.
2. `c457f99935` the view list's expansion upsert, per package (the
card's direction A):
- `servedViewExpansion` (new, module level) picks the expansion that
serves a name at a package's address. The package's own container row's
expansion comes first, then a package-less one's, never another
package's. Within one package the last expansion still wins.
- The package order is the package dimension of
`servedOverlayRowCandidates` (#21804's one prefer-local resolution, "no
second resolver").
- `readFlattenedMetaItems`' view branch serves that function's answer
for each slot of a name (the package of a listed item, or of a container
row that expands it). A slot neither reaches keeps its item.
- A package-less container's expansion standing in for a package is
stamped with that package, as a package-less row standing in is, so the
list's last pass grafts that package's envelope, as the by-name read
does.
- A list scoped to a package expands the package-less container rows as
stand-ins too, in the slots the package seats only. Membership does not
grow (#21817).
- `resolveRowlessExpandedView` reads, when it names a package, the
package-less rows as the scoped list reads them, and selects through
`servedViewExpansion`.
3. `c966e63a0a` the own-row test (#21510's one predicate,
`namesWithOwnStoredRow`) takes the package whose slot is being filled. A
row counts when it is bound to that package or package-less; with no
package, every row counts. The list asks it per slot and the by-name
read for the package it names, so it is still the one predicate both
doors ask.
- **Why it is in this PR:** without it, one package's stored row of a
form's exact name kept every other package's copy expansion of that name
out of the env-wide list. On an environment-scoped kernel the endpoints
then missed a withdrawal saved in the other package's copy, while the
by-name read naming that package served it (pin (e)).
- The done-when sentence "The endpoints do too" is only true with this
commit.
- **It can be dropped on its own at review.** If it is, the docs
sentence needs a narrower exception for that case.
4. `dc853419db` the done-when wording and the changeset. The page's
"with one exception" sentence, its remedy and its "tracked in #21967"
clause are removed; the over-close sentence stays. The
`anonymousFormIntakeWithdrawnIn` docblock says the list holds one item
per package, a package's saved container copy serving that package's
item. The released #21934 changeset is not edited;
`.changeset/21967-view-expansion-per-package.md`
(`@objectstack/metadata-protocol` patch) states the change.
## Hypotheses (PM's H1 to H4), measured at base `f76c6221ac`
- **H1, confirmed.** `protocol.ts:9070`–`:9105` derived `written` from
`expandStoredViewContainers(request.type, overlays)`, upserted by name
into one `byName` map (the last expansion wins), and `merged` dropped
every other item of the name.
- Each expansion entry carries its row's `package_id` as
`container.packageId` (`storedOverlayEntries`, `:9596`).
- The stand-in logic (`standInNames`, `unseated`) did not encode a
package-less container rule. It serves a package-less ROW of exactly the
expanded name in a list scoped to a package (#21817). A package-less
container was not expanded in a scoped list at all.
- Both loaders register a shipped container's expansions per package:
`packages/objectql/src/engine.ts:7159`–`:7165` (`registerItem(...,
ownerId)`) and `packages/metadata/src/plugin.ts:1198`–`:1210`.
- **H2, partly falsified.**
- Naming a package, `resolveRowlessExpandedView` (`:9690`) already read
only that package's rows, so for two packages' containers it served that
package's own expansion. Naming none, it served the last expansion in
row order.
- The disagreement was elsewhere: naming a package, it never consulted a
package-less container. So `getMetaItem` naming a package served that
package's shipped item while the list served the package-less copy's
expansion (pin (d), package-less case, red at base).
- **H3, no contradiction found.** Ruling by ruling:
- #21334 (triage 5946423948, seat 5955628428): the naming arm and "the
object door and the by-name read answer the same row".
`expandRuntimeViewContainer` is unchanged, and per-package slots make
the two doors agree where they did not. Its controls (a package-less
overlay of a package's own container keeps `OBJECT.default`, one item)
pass.
- #20301 (`20301-spec-view-container-name-ledger-note.md`): a
liveness-ledger note about the container body's `name` stamp. No
expansion rule; untouched.
- #21639 (triage 5973827435): the save door's one collision predicate,
and "No merge rule: that would be a second precedence order to maintain"
for two containers that collide in one selection.
- The save door is unchanged, and within one package the last expansion
still wins.
- Per-package slots apply ADR-0048's existing package resolution, not a
new precedence order.
- The save door already accepts each package's overlay of its own
container: the row under the save name is left out of the siblings. That
is the case this PR serves per package.
- #21804 (triage 5987404976): "one prefer-local resolution ... no second
resolver". The package order is read from `servedOverlayRowCandidates`
(`addressPackages`).
- #21817 (triage 5988911029): "the scoped list's membership does not
grow". A package-less container stands in only in slots the package
seats, and is never appended to a scoped list. Its landing note recorded
"a package-less stored view container is not expanded in a scoped list"
as a fact, not a ruling. Expanding it as a stand-in follows direction A
("Teach `resolveRowlessExpandedView` the same rule").
- #21442 (triage 5957375321): "No second expansion rule. No
kernel-specific branch." One selection function serves both doors, with
no kernel branch.
- #21510 (triage 5964342087): "the stored row wins on both doors ... one
predicate, read by both doors". Commit 3 keys that predicate by the
slot's package. It is still one predicate, read by both doors, and a row
still wins its own slot.
- **H4, confirmed.** `findPublicFormView`
(`packages/rest/src/rest-server.ts:10735`) and `resolveFormBySlug`
(`:10784`–`:10819`) read the organization's list and the env-wide list.
`anonymousFormIntakeWithdrawnIn`
(`packages/metadata-core/src/anonymous-form-intake.ts:336`) compares
names, not packages. So once the list holds each package's item, the
shared verdict judges each package's withdrawal. The organization-scoped
save check's first judgment (`anonymousFormIntakeReopenRefusal`) reads
the same env-wide list. No `rest-server.ts` edit.
## Pins
(`packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts`,
23 cases)
They are driven through the real protocol reads, as the file's existing
cases are. The doors are modelled by their own composition (the
organization's list, each open candidate judged over the env-wide list).
The registry double holds each package's shipped container and its
expanded views under `PACKAGE:NAME`, as the real `SchemaRegistry` holds
them. Every case runs on an environment-scoped and an unscoped kernel.
- (a) package A's env-wide copy saved with the form open, package B
shipping it withdrawn: the env-wide list holds B's withdrawal beside A's
item, and the doors serve no copy of a restored organization overlay.
Run with no code package owning the object, and with A owning it.
Control: with no package withdrawing it, the doors serve the overlay.
- (b) the same with B's withdrawal saved in B's own copy, both save
orders.
- (c) a package-less copy stands in for every package with no copy of
its own; a package's own copy serves that package's slot ahead of it,
both save orders.
- (d) for a name two packages' copies expand, and for a package-less
copy: each package's slot in the env-wide list, `getMetaItem` naming the
package, and the list scoped to the package serve the same item.
- (e) one package's stored row of the form's exact name serves that
package's slot and hides no other package's copy. Control: a
package-less row of the name serves every slot.
No dogfood case was added: the doors' verdict is a pure composition of
`getMetaItems` and the shared function, pinned in-process as #21934's
pins are.
## Measured
- **Base reading** (pins at `2981f6eb78` over base `protocol.ts`): 19
red, 4 green. The greens are the (a) control, both (e) controls, and (e)
on the unscoped kernel, where registry hydration served B's copy. Every
red failed in the defect's shape, for example `expected [ [ 'pkg_a',
true, … ] ] to deeply equal [ [ 'pkg_a', … ], [ 'pkg_b', … ] ]`.
- **Reverse verification** on committed head `dc853419db`, through
`scripts/ablation-replace.mjs`. The anchor hit once, the blob changed on
disk, and each restore was proved (blob equal to HEAD `def1f5213f34`,
`git diff HEAD` empty). The subject is imported from source
(`./protocol.js`), so there is no `dist` leg. Predictions were written
before the runs.
- A1, the order's ablation (the view branch set back to base's by-name
upsert, verbatim): predicted 19 red / 4 green, measured 19 red / 4
green, the same split as the base.
- A2 (the own-row test asked with no package): predicted 1 red, measured
1 red: (e) on the environment-scoped kernel.
- A3 (the by-name read without its package-less read): predicted 2 red,
measured 1 red, on the environment-scoped kernel. The prediction missed
the unscoped kernel: there the by-name read falls through to the
registry, whose bare-name `getItem` answers the hydrated expansion with
the same body.
- **Clause-②** measured on the built entry declarations, base
`protocol.ts` against head, comments stripped:
- `@objectstack/metadata-protocol` `dist/index.d.ts` and `index.d.cts`:
identical (2162 lines each).
- `@objectstack/metadata-core` `index.d.ts`, `index.d.cts`,
`repository-*.d.ts` and `testing.d.ts`: identical.
- The raw bytes differ in comments only. No exported signature moves, so
`Clause-②: no`, as claimed.
- **Tests:**
- `@objectstack/metadata-protocol` at `c966e63a0a` (later commits change
no source in it): `typecheck` green (`tsc --noEmit`). Full suite in two
halves: 218 files passed, 3 skipped; 28019 tests passed, 19 skipped.
- The related suites again after the own-row commit: 8 files, 884 tests
passed.
- `@objectstack/metadata-core` (comment only): both `typecheck` programs
green; 18 files, 411 tests passed. The edited test file is in
metadata-protocol's tsc program (counted with `--listFiles`).
- **Gates**, all at head `dc853419db` (`origin/main` is still the base
`f76c6221ac`, so the merge is a no-op):
- `node scripts/pm/dispatch-gates.mjs --commands` with no paths derived
93 commands from the 5 changed paths. All 93 were run and exit 0.
`--ran` with recorded exit codes: "93 derived, 93 run, 0 NOT-MEASURED, 0
UNRUN".
- Three of them first answered `PREREQUISITE NOT MET` (unbuilt workspace
packages): `check:skill-examples`, `check:lean-entry-closure` and
`check:dual-build-cjs-loads`. `check:type-check-debt`'s own re-measure
then built the workspace, and all three were rerun green: 262 examples,
2 conditions, 106 entry points in 66 packages.
- The artifact-roster block printed outside the total (53 commands) was
run as well. 51 exit 0. `check-closing-target-claim` and
`check-single-claim-paths` need this PR's context, so they run after it
opens (in the report). `check-partof-closing-keyword` ran green over
this body.
- The four symbol-anchor sweeps are green: `check:adr-symbol-anchors`,
`check:scripts-symbol-anchors`, `check:spec-docblock-symbol-anchors` and
`check:adr-anchors`.
- Lint, narrowed to the 3 touched TypeScript files with `eslint
--no-inline-config` (as `pnpm lint` runs it): 3 files, 0 errors, 0
warnings.
- The `.mdx` and the changeset match no eslint `files` glob.
- `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project` or `projectService`), and it reads only two
baselines from disk, neither touched. So the diff cannot move an
untouched file's verdict.
- No raw control byte in any changed file (`grep -P` over the five
paths: 0 each). `check:nul-bytes` is green.
## Acceptance notes
- The save door's collision predicate (#21639) reads sibling expansions
with no package filter. So two different containers of two packages that
expand one name are still refused at save, though the read doors would
now serve them in separate slots. Stricter than the readers, and it
fails closed: noted, not changed.
- A by-name read that names no package still answers any row's expansion
(the last one), as `servedOverlayRowCandidates` does for no package.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4e4e881 commit db87a02
5 files changed
Lines changed: 494 additions & 55 deletions
File tree
- .changeset
- content/docs/ui
- packages
- metadata-core/src
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
71 | | - | |
| 71 | + | |
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
326 | 326 | | |
327 | 327 | | |
328 | 328 | | |
329 | | - | |
330 | | - | |
331 | | - | |
332 | | - | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
333 | 332 | | |
334 | 333 | | |
335 | 334 | | |
| |||
0 commit comments