Skip to content

Commit 8d017eb

Browse files
hotlongclaude
andauthored
fix(metadata-protocol): fold publishMetaItem through canonicalizeMetaRequestType (#8769) (#8825)
* fix(metadata-protocol): fold publishMetaItem through canonicalizeMetaRequestType * test(objectql): pin the #8769 publish fold, and correct two claims the draft carried The implementation commit (8611170) was written by an agent that was killed before it could run anything, so its comment stated intent as measurement. This commit re-derives every claim on the merged tree and corrects the two that did not survive contact. Corrected in `protocol.ts`: * `ensureObjectStorage` was listed among the sites that "keyed the caller's spelling". It does not: it opens `if (type !== 'object' && type !== 'objects')` and so answered both spellings identically before the fold and after it. Its `'objects'` limb is now unreachable (both call sites stand behind a fold) but it is out of this card's region, so it is recorded here and filed as #8820 rather than deleted in passing. * The bullet now names what the fold does move — the ADR-0010 audit row and the receipt sentence, which both read `request.type` and so recorded the caller's spelling for a row written under the canonical one. The pin lands in `packages/objectql` rather than beside the code because `metadata-protocol` cannot import `ObjectQL` (objectql depends on metadata-protocol, not the reverse), and this seam is only observable against a real engine and repository. It is the same package, and the same reason, as the existing fold pins for the other six entry points in `protocol-meta-type-canonicalization.test.ts`. Reverse verification, direction predicted before running and measured against a REBUILT dist (the suite resolves the package through `dist`, so a source-only revert measures nothing): predicted 3 red / 4 green, measured 3 red / 4 green, each red for its predicted reason — including group C, where the ablated build published straight through an ADR-0010 `_lock` that the canonical spelling is refused by. Filed while measuring, both out of region and neither fixed here: #8819 — `rollbackMetaItem` is an eighth `/meta` entry point with the same unfolded lock gate, reachable via POST /api/v1/meta/:type/:name/rollback #8820 — `ensureObjectStorage`'s now-dormant `'objects'` limb Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 03a8da0 commit 8d017eb

3 files changed

Lines changed: 531 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
fix(metadata-protocol): route `publishMetaItem` through the `/meta` canonical-type fold (#8769)
6+
7+
`canonicalizeMetaRequestType` is the `/meta` request boundary, and its own
8+
header describes it as the fold "all six entry points funnel through".
9+
`publishMetaItem` is a **seventh** entry point on the same URL family
10+
(`/api/v1/meta/:type/:name/publish` and the `…/published` overlay) and did not
11+
funnel through it: it reached the draftability check through
12+
`PLURAL_TO_SINGULAR`, the MANIFEST-COLLECTION map, which is the exact lookup
13+
#7894 replaced at the other six. One contract, two dialects, decided by which
14+
verb you used (Prime Directive #12).
15+
16+
The fix is the same one line the other six carry, at the top of the method. What
17+
that line reaches — measured on `origin/main`, not inferred — differs by whether
18+
the type is in the manifest map, and the two halves are not the same severity:
19+
20+
**The four manifest-absent types — fail-closed, but closed for the wrong reason
21+
and with the wrong verdict.** `field`, `seed`, `external_catalog` and
22+
`translation` are legitimately absent from `PLURAL_TO_SINGULAR` (they are not
23+
stack collections; that absence is precisely why #7894 moved the boundary onto
24+
the URL map). Unfolded, they arrived at the draftability check as unrecognised,
25+
where `isRuntimeCreateAllowed`'s "no static registry entry ⇒ this is a
26+
plugin-registered kind" arm answers **true** — the permissive plugin branch,
27+
taken for a type the platform itself declares. So a publish addressed
28+
`/meta/fields/showcase_task.title` PASSED a gate that `/meta/field/...` answers
29+
`403 NOT_OVERRIDABLE`, and only failed further down, on `404 no_draft`, having
30+
already forgotten which type it was judging. A publish addressed
31+
`/meta/translations/zh_cn` likewise never resolved the draft that
32+
`PUT /meta/translations/zh_cn` had folded and written under `translation`. After
33+
the fold: the first is refused `403 NOT_OVERRIDABLE` by its real registry entry,
34+
the second promotes the row it names.
35+
36+
**Manifest-present types — one lookup that did NOT fail closed.**
37+
`promoteDraftForPublish` folds through the manifest map before the row lookup,
38+
so a publish addressed `/meta/views/case_grid` always resolved the canonical
39+
row. `getEffectiveLock` does not agree with it: its artifact limb folds, its
40+
**overlay limb queries `sys_metadata` with the raw `type`**. Addressed with the
41+
plural, the ADR-0010 `_lock` carried by the stored active row was looked up
42+
under a `type` no row has and came back `'none'` — which is not a neutral value,
43+
it is the verdict "the author declared no protection" (#5706) — while the
44+
promote one line later read the folded key and overwrote the row the lock
45+
protected. Measured on `origin/main`: `_lock: 'no-overlay'` plus a pending
46+
draft, canonical spelling `403 ITEM_LOCKED`, plural spelling **200 and the
47+
active body replaced**.
48+
49+
That window is narrow and is stated at its real width rather than rounded up: it
50+
needs an environment kernel (the gate is skipped wholesale when `environmentId`
51+
is `undefined`), a lock carried by a *stored overlay* row rather than a packaged
52+
artifact, and a draft that predates the lock — because the save door refuses to
53+
mint one once the lock is live. It is nevertheless a lock gate that could be
54+
addressed around from the wire, and "a lock gate must not fail open" is the rule
55+
this file already carries.
56+
57+
`promoteDraftForPublish`'s own `PLURAL_TO_SINGULAR` fold is **kept**, and the
58+
measurement is the reason: that helper's other caller is `publishPackageDrafts`,
59+
which feeds it stored row types. That is data at rest, where a legacy row
60+
written under a plural `type` is real and nothing rewrites it on upgrade — a
61+
different input class needing a different map, exactly as `canonicalMetaType`'s
62+
header describes. Deleting it as "now redundant" would have changed the batch
63+
path.
64+
65+
`publishPackageDrafts` and `deletePackage` need no fold of their own: neither
66+
takes a caller-supplied `type` at all (both are addressed by `packageId`), and
67+
the per-row work they delegate is already covered — `deletePackage` routes every
68+
row through `deleteMetaItem`, which folds, and `publishPackageDrafts` reaches
69+
the manifest-map fold described above.
70+
71+
The audit row and the publish receipt now record the canonical type too; both
72+
read `request.type`, so a publish addressed `/meta/views/case_grid` previously
73+
wrote `type='views'` into `sys_metadata_audit` for a row stored under `view`,
74+
and a compliance query on the canonical spelling did not find it.
75+
76+
Pinned in `packages/objectql/src/protocol-publish-canonical-fold.test.ts`
77+
against a real engine and repository, with the reverse verification's direction
78+
predicted before it was run: predicted 3 red / 4 green, measured 3 red / 4
79+
green, each red for its predicted reason.

packages/metadata-protocol/src/protocol.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12657,6 +12657,50 @@ export class ObjectStackProtocolImplementation implements
1265712657
*/
1265812658
projectionApplied?: MutationProjectionOutcome;
1265912659
}> {
12660+
// #4432 — CANONICAL TYPE KEY. See {@link canonicalMetaType}. This is the
12661+
// SEVENTH `/meta` entry point, and until #8769 it was the only one that
12662+
// did not funnel through the boundary fold — so the URL family
12663+
// `/meta/:type/:name/publish` accepted a spelling that `PUT` folds and
12664+
// `publish` did not resolve. Two dialects of one contract, decided by
12665+
// which verb you used (Prime Directive #12).
12666+
//
12667+
// What the fold reaches here that `promoteDraftForPublish`'s own
12668+
// `PLURAL_TO_SINGULAR` line does NOT — measured, not assumed:
12669+
//
12670+
// • the four types the MANIFEST map legitimately omits (`field`,
12671+
// `seed`, `external_catalog`, `translation` — they are not stack
12672+
// collections). Unfolded, they arrive at the draftability check as
12673+
// unrecognised and take the permissive PLUGIN branch of
12674+
// `isRuntimeCreateAllowed` instead of their real registry entry: a
12675+
// publish addressed `/meta/fields/...` passed a gate that
12676+
// `/meta/field/...` answers 403 NOT_OVERRIDABLE. The #7894 shape,
12677+
// one verb over.
12678+
// • `getEffectiveLock`'s OVERLAY limb, which queries `sys_metadata`
12679+
// with the raw `type`. Its artifact limb folds; the overlay limb
12680+
// does not, so an ADR-0010 `_lock` carried by the stored active row
12681+
// was not found when the publish was addressed with a plural — while
12682+
// the promotion below DID find the row, because it reads the folded
12683+
// `singularType`. That asymmetry is the one thing here that was not
12684+
// fail-closed.
12685+
// • the ADR-0010 audit row and the receipt sentence, which both read
12686+
// `request.type` and so recorded the CALLER's spelling for a row
12687+
// written under the canonical one — a compliance query on
12688+
// `type = 'view'` missed a publish addressed `/meta/views/…`.
12689+
//
12690+
// ⚠️ `ensureObjectStorage` is NOT in that list, though it also reads
12691+
// `request.type`: it opens `if (type !== 'object' && type !== 'objects')`
12692+
// and so answered both spellings identically before this fold and after
12693+
// it. Its `'objects'` limb is now unreachable — both of its call sites
12694+
// stand behind a fold — but it is a spelling-tolerant lookup one layer
12695+
// down, which is the shape {@link canonicalMetaType}'s header rejects,
12696+
// so it is recorded rather than quietly deleted here (out of region).
12697+
//
12698+
// ⛔ This does NOT make `promoteDraftForPublish`'s fold redundant — that
12699+
// helper's other caller is `publishPackageDrafts`, which feeds it stored
12700+
// row types (data at rest, where a legacy plural row is real and nothing
12701+
// rewrites it on upgrade). Different input class, different map; see
12702+
// {@link canonicalMetaType}'s header for why the two are not one fold.
12703+
request = canonicalizeMetaRequestType(request);
1266012704
// [#8594] The refusal's own row is written HERE, by the route that owns
1266112705
// the (absent) transaction — see `promoteDraftForPublish`'s header. This
1266212706
// site has no transaction of its own, so recording it in the `catch` is

0 commit comments

Comments
 (0)