Repository navigation
Commit 3b49318
Fixes #22128
Clause-②: yes (widening)
The fix restores ADR-0008's stated behaviour: an unpinned save is
last-writer-wins. The `packageId` describe now states what the code
already does.
`@objectstack/metadata-protocol` is `minor`, because
`SysMetadataRepository.headAt` is one added public method on an exported
class. It is the only change to the package's public surface. The
precedent is
`.changeset/21986-metadata-protocol-declines-stored-row-public.md`: one
added method, `yes (widening)`, `minor`. `@objectstack/rest` and
`@objectstack/spec` stay `patch`.
The contract review (`6053073603`) found the earlier declaration of `no`
wrong on this one point. Triage's `no` came before the fix had a shape.
A widening is not a break, so no ADR-0087 disposition marker applies:
`check-adr-0087-registration` reads `1 non-breaking changeset(s)`.
## Direction (triage `6049925243`, binding)
> One function resolves "the draft head for this address". The save's
expected-parent read, the repository's lock comparison and the read's
served `version` all call it.
>
> It resolves a package-less draft over a package-bound active row the
way the repository already does. That inheritance is #11087's fix,
pinned by `sys-metadata-repository.draft-package-inherit.test.ts`.
>
> ⛔ Not by dropping the inheritance: that re-opens #11087 (drafts lose
their package).
>
> The `packageId` describe, which says absent means env-local, is
aligned in the same PR to state the inheritance.
The seat's rider `6051335917` adds two items: `?package=all` on the item
read, and the `metadata_conflict` spelling in the publish route's
description. Patch round 1 adds a third member of the class, which the
contract review escalated: the credential carry-forward. See [the
section below](#the-credential-carry-forward-patch-round-1).
## What was wrong (measured before the fix)
The new real-route test
`packages/rest/src/meta-draft-head-package-inheritance.test.ts` was
committed first (`eb42ce01e`). It ran against a `dist/` built at the
base `7ef50a4fb`, which already carries PR #22126:
```
pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/meta-draft-head-package-inheritance.test.ts
Tests 6 failed | 1 passed (7)
step 3: {"error":"view/case_grid has been modified since you loaded it. Expected parent null but current is hmac-sha256:1de899d7…","code":"METADATA_CONFLICT",…}: expected 409 to be 200
?state=draft after step 2: expected null to be 'hmac-sha256:1de899d7…'
?package=all read: expected null to be 'hmac-sha256:6df7b221…'
?state=draft&package=all: {"error":"No pending draft exists for view/case_grid.","code":"NO_DRAFT"}: expected 404 to be 200
```
The one case that passed is the env-local control, which is meant to
pass both before and after.
The three readers resolved three different rows:
| reader | before | after |
|---|---|---|
| the save's expected parent (`saveMetaItem`, through `storedHeadAt`) |
`repo.get` at the key it was handed. With no package, that is the
package-unbound row (`package_id IS NULL`). | `repo.headAt`, which runs
`resolveWriteHead` |
| the read's `version` (`readVersionToken`, through `storedHeadAt`) |
the same `repo.get`. The REST read door forwarded `?package=all`
verbatim, so the key was `package_id = 'all'`. | the same `repo.headAt`.
`all` is folded first, as the save door folds it. |
| the repository's lock (`SysMetadataRepository.put`) | the active row's
package, inherited for a draft that names no package (#11087), else the
named key, with orphan-draft adoption as a fallback |
`resolveWriteHead`, the same code moved into one function |
## The change
- **The one resolution: `SysMetadataRepository.resolveWriteHead`**
(private, `packages/metadata-protocol/src/sys-metadata-repository.ts`).
For a write of `state` that names `packageId`, it returns the row the
write upserts (`null` for a create) and the binding a create stamps. It
contains #11087's draft inheritance, which reads the active row across
the caller's org and the env-wide scope, and the orphan-draft adoption,
both unchanged. It lives in the repository because `put`'s lock and
stamp already live there, and the protocol already calls the repository.
Calling it from the protocol adds no new layer dependency. Keeping it in
the protocol would have made the repository call up into its caller.
- `put` calls it inside its transaction, for the lock and the stamp. The
inheritance lookup now runs inside that transaction too. It used to run
just before it.
- `SysMetadataRepository.headAt(ref, { state, packageId })` is the new
public method. It answers `{ head, packageId }`. `head` is the row the
write upserts, served as `get` serves a row, so its `hash` is the
version `lockAccepts` accepts. `packageId` is the binding that write
targets.
- **The callers rewired.**
- `ObjectStackProtocolImplementation.storedHeadAt` now asks
`repo.headAt`, not `repo.get`. Its callers are unchanged:
- `saveMetaItem`'s expected-parent read, for both the `draft` and the
`publish` mode. Unpinned, the parent is the head. Pinned, the token is
judged against the head (`storedParentForToken`).
- `readVersionToken`, the item read's `version`, for both `?state=draft`
and the uncached plain read.
- `storedBodyForCarryForward` (patch round 1). Both of its reads now ask
`repo.headAt`: the row the save overwrites, and with no draft yet the
active row in the package the draft is stamped into.
- REST: `metaItemPackageBinding(raw)`
(`packages/rest/src/rest-server.ts`) is the one reading of `?package=`
on the three `/meta/:type/:name` doors. Before, `PUT` and `POST
…/publish` each had an inline copy, and `GET` had none. `all` and the
empty value name no package. `GET`'s cache bypass (`packageScoped`)
still reads the raw parameter, so `?package=all` keeps the uncached arm
and keeps serving `version`.
- **Spec, descriptions only.**
- `SaveMetaItemRequestSchema.packageId` now states the inheritance. It
also states that the same row is the one the parent-version resolution
reads and the one the item read serves as `version`. The protocol
reference is regenerated (`gen:docs`, through `check:generated --fix`).
- The `POST /:type/:name/publish` route description now says 409
`METADATA_CONFLICT`.
- `GetMetaItemResponseSchema.version`'s describe (from #22114) was
re-read against the fix and still holds: "the STORED row a save to this
item compares against, at this read's scope". It is unchanged.
One behaviour moves on `GET /meta/:type/:name?package=all`: the served
content. Before, `all` was a package address, so the read preferred a
row bound to a package literally named `all`, then the package-less row.
Now the read serves what a read with no `?package=` serves. That
includes the draft the package-less draft save stored in the item's
package, which before answered `404 NO_DRAFT`. objectui's editor sends
this read (read at objectui `9990f9e`): `ResourceEditPage.tsx` scopes
its draft and layered reads with the router's raw `?package=`, which
does not fold `all` (its own docblock says so), while its saves fold it.
## The credential carry-forward (patch round 1)
**The defect.** A save of a type whose read withholds a credential
carries the stored credential back into a body that returns as it was
served. Today those types are `datasource` (built in) and `flow` (with
the automation plugin). The carry-forward is #8154's write-path inverse.
`storedBodyForCarryForward` compared the served body against `repo.get`
at the key the caller named. For a package-less draft save of a
package-owned item, neither the inherited draft nor the package-bound
active row sits at that key. So the comparison fell to the code layer,
nothing was carried, and the stored credential was persisted away.
**Measured on the real route** (the rest harness above, better-sqlite3),
at head `07c229054`, before this round's fix:
- The setup is a package-owned legacy datasource row whose `config.url`
holds a userinfo credential. It was written straight to the store,
because the save door now refuses such a URL, so a row like this only
exists from before the gates.
- `GET /meta/datasource/warehouse` serves it redacted.
- That body is saved back with `PUT …?mode=draft` and no `?package=`,
twice: 200, 200.
- Each time, a system read of the stored draft row (bound to the
package) shows `config.url` with no credential.
- The env-local control kept the credential in both drafts.
`meta-draft-head-package-inheritance.test.ts` went `1 failed | 8 passed
(9)` on the pre-fix `dist/`. The failure is `expected [ false ] to
deeply equal [ true ]`, on whether the first stored draft holds the
credential. With the fix and `metadata-protocol` rebuilt (`grep -c
'target.packageId' dist/index.js` → 1), the file is green. Neither the
PR body nor any comment carries a credential value: the pins assert a
boolean.
**The fix** is the same resolution (`4e63f1122`).
`storedBodyForCarryForward` reads `repo.headAt` at the save's own state.
With no draft yet, it reads `repo.headAt(active, packageId)` with the
binding that call answered. The code-layer fallback and its `#20552`
reason are unchanged. An active save's carry-forward, and the authoring
gate's `restoredCredentialPathsFor`, read the exact key as before,
because `headAt` adds nothing for `active`.
## Pins
`packages/rest/src/meta-draft-head-package-inheritance.test.ts`: the
real `RestServer` routes, better-sqlite3 `:memory:`, the real
`sys_metadata*` objects. Each pin reads the store back.
- Triage's sequence: `the measured sequence: active save in a package, a
package-less draft, the same draft again with no If-Match → 200, 200,
200`
- Triage's read pin: `?state=draft after the package-less draft serves
that draft's version (the save receipt's), not null`
- Triage's controls:
- `control: a stale If-Match is still refused 409, with the inherited
draft's token as currentVersion`. It also covers `If-None-Match: *`
refused while the inherited draft exists.
- `control: an env-local item with no package is unchanged — unbound
rows, unpinned saves accepted, the read serves the receipt's token`
- Rider pins (`?package=all`):
- `active: the read's version is the token the save compares against;
If-None-Match: * is refused only because a row is there`
- `active, no row at the env-local address: the read serves null, and
If-None-Match: * creates it`
- `draft of a package-owned item: ?state=draft&package=all serves the
inherited draft and its token, and the save accepts it`
- Carry-forward pins (patch round 1), describe `[#22128] the credential
carry-forward reads the row the draft save overwrites`:
- `package-owned: the redacted read saved back as a package-less draft,
twice, keeps the stored credential`
- `control: env-local, the same round trip keeps the stored credential,
as before`
`packages/metadata-protocol/src/sys-metadata-repository.draft-package-inherit.test.ts`,
new describe `SysMetadataRepository.headAt — the row put locks against
(#22128)`, 6 cases. They also pin the `packageId` binding `headAt`
answers.
- the inherited draft is the head, `get` at the named key is not, `put`
refuses parent `null` and accepts the head's version;
- the orphan draft is the head;
- an explicit package never resolves another package's row;
- an active address inherits nothing;
- an org-scoped draft address resolves across the ADR-0005 reach;
- a brand-new item drafted first has no head and the package-less
binding.
The #11087 inheritance pins in that file are unchanged and green.
## Tests and gates
Every reading below is at head `ea3c748f8` (patch round 1).
`origin/main` was not merged again, because the branch still merges
cleanly. No file of this diff overlaps the 11 commits `main` has gained
since `959c209d5`. `@objectstack/metadata-protocol` was rebuilt before
the real-route runs, because `@objectstack/rest`'s tests resolve it
through `exports` to `dist/`.
- **The real-route pins.**
`meta-draft-head-package-inheritance.test.ts`,
`meta-item-version-token-occ.test.ts` and
`meta-publish-package-scope.test.ts` → `Test Files 3 passed (3) · Tests
42 passed (42)`, run at `4e63f1122`, whose code `ea3c748f8` carries
unchanged. That covers this card's 9 pins plus #22114's OCC pins and the
`PUT`/publish `?package=` parity pins.
- Before round 0's fix, the first 7 pins were `6 failed | 1 passed`, as
quoted above.
- Before round 1's fix, the carry-forward pin was `1 failed | 8 passed
(9)`, as stated in its section.
- **`@objectstack/metadata-protocol`.**
- `vitest run --maxWorkers=2` → `Test Files 221 passed | 3 skipped (224)
· Tests 28259 passed | 19 skipped`. That includes
`protocol.metadata-redaction.test.ts` and
`protocol.credential-channel.test.ts`.
- `typecheck` → exit 0.
- **`@objectstack/rest`.**
- `vitest run --project local --maxWorkers=2` → `Test Files 262 passed
(262) · Tests 4938 passed | 326 skipped`.
- `typecheck` (`tsc --noEmit && check:test-typecheck`) → exit 0.
- **Downstream consumer, `@objectstack/objectql`.** The 54 test files
that name `saveMetaItem` / `getMetaItem` / `SysMetadataRepository` → `54
passed · 683 tests passed`. `protocol-meta.test.ts`'s `findOne` pins are
among them.
- **`@objectstack/spec` (descriptions only, no source change since round
0).**
- Round 0's readings at `07c229054`: the 4 test files that name
`SaveMetaItemRequestSchema` or the REST route table → `4 passed`, and
`typecheck` → exit 0.
- `check:generated` was re-run at this head and is green.
- **The changeset gates.**
- `check-changeset-no-major` was run with a `pull_request` event
carrying this body. It answered `LEVEL AXIS: this PR declares clause-②
yes (widening), and it grades a package whose packages/**/src/** it
moves at minor or above`, exit 0.
- `check-adr-0087-registration` → `1 non-breaking changeset(s)`, exit 0.
- **Gates.**
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `ea3c748f8` derived 113 commands, the
same set as round 0. Each was run with its exit code captured before any
pipe. `--ran` gives: `113 derived famil(ies) accounted for — 111 run, 2
NOT-MEASURED`.
- NOT MEASURED (exit 3, prerequisite not met, both need a full-repo
build, CI runs them):
- `pnpm --filter @objectstack/spec run check:skill-examples` (no
`client-react` dist);
- `pnpm check:dual-build-cjs-loads`.
- The derivation flagged the tree as 11 commits behind `origin/main`.
Two gate-source files differ there
(`scripts/engine-double-contract.pinned.json`, which adds no entry for
this diff's files, and a removed audit probe).
- The 48-family artifact-roster block was run too, including
`check:error-code-provenance`, `check:error-code-casing`,
`check:engine-double-contract`, `check:spec-changes` and
`check:meta-url-spelling`. 45 exit 0. The three PR-context gates are
`check-partof-closing-keyword`, `check-closing-target-claim` and
`check-single-claim-paths`. They were run with `PR_NUMBER=22185` and
this body, and each exits 0.
## Acceptance notes
Each item below is noted, not filed.
- **The same `?package=all` reading on three more `/meta` doors
(measured).** On the real stack, with a view whose active row is stored
in package `com.probe.pkg`:
- `GET /meta/view/case_grid/layers` → 200, overlay `live`. `GET
/meta/view/case_grid/layers?package=all` → **404**.
- `GET /meta/view` → `["case_grid"]`. `GET /meta/view?package=all` →
`[]`.
The layered read (`rest-server.ts`, `layeredPackageId =
req.query?.package || undefined`), the list read and the book lookup
each forward the literal `all`. objectui's editor
(`ResourceEditPage.tsx` at objectui `9990f9e`) scopes its layered and
draft reads with the router's raw `?package=`. These doors are outside
this card's declared surface, so they are not changed here. The seat
filed them as #22188. `metaItemPackageBinding` is the fold they would
share.
- **Lowercase `metadata_conflict` elsewhere.** The spelling persists in
`packages/client/src/index.ts` (four docblocks),
`packages/cli/src/commands/meta/delete.ts:115` (a description string),
`content/docs/concepts/metadata-lifecycle.mdx:147` and
`docs/qa/platform-checklist/areas/studio-authoring.json:374,376`. The
rider named only the route description. `sys_metadata_audit`'s `code:
'metadata_conflict'` is the audit vocabulary's data value and is correct
as it stands.
- `put`'s draft-inheritance lookup now runs inside its transaction,
beside the existing-row read. It used to run just before the
transaction. The predicates are unchanged.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 35396b5 commit 3b49318
9 files changed
Lines changed: 664 additions & 111 deletions
File tree
- .changeset
- content/docs/references/api
- packages
- metadata-protocol/src
- rest/src
- spec/src/api
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2601 | 2601 | | |
2602 | 2602 | | |
2603 | 2603 | | |
2604 | | - | |
| 2604 | + | |
2605 | 2605 | | |
2606 | 2606 | | |
2607 | 2607 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8586 | 8586 | | |
8587 | 8587 | | |
8588 | 8588 | | |
8589 | | - | |
8590 | | - | |
| 8589 | + | |
| 8590 | + | |
8591 | 8591 | | |
8592 | 8592 | | |
8593 | 8593 | | |
8594 | 8594 | | |
| 8595 | + | |
| 8596 | + | |
| 8597 | + | |
| 8598 | + | |
| 8599 | + | |
| 8600 | + | |
| 8601 | + | |
| 8602 | + | |
8595 | 8603 | | |
8596 | 8604 | | |
8597 | 8605 | | |
| |||
8621 | 8629 | | |
8622 | 8630 | | |
8623 | 8631 | | |
8624 | | - | |
8625 | | - | |
| 8632 | + | |
| 8633 | + | |
| 8634 | + | |
8626 | 8635 | | |
8627 | 8636 | | |
8628 | 8637 | | |
| |||
8635 | 8644 | | |
8636 | 8645 | | |
8637 | 8646 | | |
8638 | | - | |
| 8647 | + | |
| 8648 | + | |
| 8649 | + | |
| 8650 | + | |
8639 | 8651 | | |
8640 | 8652 | | |
8641 | 8653 | | |
| 8654 | + | |
8642 | 8655 | | |
8643 | | - | |
| 8656 | + | |
8644 | 8657 | | |
8645 | | - | |
8646 | | - | |
| 8658 | + | |
| 8659 | + | |
8647 | 8660 | | |
8648 | 8661 | | |
8649 | 8662 | | |
| |||
18039 | 18052 | | |
18040 | 18053 | | |
18041 | 18054 | | |
18042 | | - | |
18043 | | - | |
18044 | | - | |
| 18055 | + | |
| 18056 | + | |
18045 | 18057 | | |
18046 | 18058 | | |
18047 | | - | |
18048 | | - | |
18049 | | - | |
18050 | | - | |
18051 | | - | |
| 18059 | + | |
| 18060 | + | |
| 18061 | + | |
| 18062 | + | |
| 18063 | + | |
| 18064 | + | |
| 18065 | + | |
| 18066 | + | |
| 18067 | + | |
| 18068 | + | |
| 18069 | + | |
| 18070 | + | |
| 18071 | + | |
| 18072 | + | |
| 18073 | + | |
18052 | 18074 | | |
18053 | 18075 | | |
18054 | 18076 | | |
18055 | | - | |
| 18077 | + | |
18056 | 18078 | | |
18057 | 18079 | | |
18058 | 18080 | | |
18059 | | - | |
| 18081 | + | |
18060 | 18082 | | |
18061 | 18083 | | |
18062 | 18084 | | |
| |||
18075 | 18097 | | |
18076 | 18098 | | |
18077 | 18099 | | |
18078 | | - | |
| 18100 | + | |
| 18101 | + | |
| 18102 | + | |
| 18103 | + | |
| 18104 | + | |
18079 | 18105 | | |
18080 | 18106 | | |
18081 | 18107 | | |
| |||
18088 | 18114 | | |
18089 | 18115 | | |
18090 | 18116 | | |
18091 | | - | |
| 18117 | + | |
18092 | 18118 | | |
18093 | 18119 | | |
18094 | 18120 | | |
| |||
20932 | 20958 | | |
20933 | 20959 | | |
20934 | 20960 | | |
20935 | | - | |
| 20961 | + | |
| 20962 | + | |
| 20963 | + | |
20936 | 20964 | | |
20937 | 20965 | | |
20938 | 20966 | | |
| |||
0 commit comments