Skip to content

fix(metadata-protocol): declare packageId on publishMetaItem's request type, and correct three comments that say the per-item door names no package - #11005

Merged
os-elon merged 3 commits into
mainfrom
claude/issue-10350-publish-request-shape
Aug 22, 2026
Merged

fix(metadata-protocol): declare packageId on publishMetaItem's request type, and correct three comments that say the per-item door names no package#11005
os-elon merged 3 commits into
mainfrom
claude/issue-10350-publish-request-shape

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#10350

publishMetaItem's declared request type carried type / name / organizationId / actor / message / _skipSeedApply and no packageId, while since #10063
POST /meta/:type/:name/publish?package=PKG_ID states one on every HTTP-driven
promotion that names a package. Three in-tree comments still claimed the per-item
door "names no package".

Nothing was broken at runtime, and this does not repair a live bug. The value
already flowed end to end: the request object is forwarded whole, the one
transform in between (canonicalizeMetaRequestType) is a spread that drops no
key, and promoteDraftForPublish already declared packageId?: string | null.
The defect was on the declared contract — invisible to typed callers, and one
destructuring refactor away from being dropped in silence.

What changed

  1. publishMetaItem declares packageId?: string | null
    (packages/metadata-protocol/src/protocol.ts), with the absent-vs-null
    distinction documented: an absent key keeps the historical "match any
    package" resolution, null pins the lookup to the unbound row, and a
    present-and-undefined key coerces to null downstream and makes a
    package-bound draft unfindable.
  2. Three stale comments corrected — two in promoteDraftForPublish and a
    third in resolveDraftOrgScopeForPublish that the card did not name but
    carries the identical stale claim.
  3. Three pins in protocol-publish-drafts-package-scope.test.ts.

The fence inside (3) was honoured

The draftForGate / rowToItem warning those comments were really defending —
that the package must not be read off draftForGatesurvives verbatim,
proved byte-for-byte against HEAD:

diff <(git show HEAD:...protocol.ts | sed -n '/Deliberately NOT read off/,/contract\./p') \
<(sed -n '/Deliberately NOT read off/,/contract\./p' ...protocol.ts)
DIFF_EXIT=0

Only the "names no package" claim was rewritten. Both surviving matches for that
phrase in the file are now the quoted-and-corrected forms.

environmentId is deliberately NOT added — this diverges from the card

The card's ⚠️ asked for the declared shape, noting environmentId sits in the
same cast-hidden position at the call site. That is true of the call site, but
the conclusion is already ruled on: environmentId is out of the protocol
request shape by the maintainer ruling recorded 2026-08-18 on #9741
, quoted in
packages/rest/src/rest-server.ts:

environmentId is the multi-kernel routing key, and it is OUT of the protocol
request shape by explicit maintainer decision ... Never add protocol
members here; a key that belongs to the request belongs in the spec schema.

Measured independently: request.environmentId occurs nowhere in
packages/metadata-protocol — the class reads its environment off the instance
(private environmentId?: string, set at construction). Adding it would reverse
that ruling rather than record it, so the exclusion is documented in-code
instead. Flagged for the maintainer rather than decided quietly.

Why the (p as any) cast could NOT be removed

The card called the cast the finding — "it is what makes the call compile, so the
contract is currently enforced by nothing". The first half is true; the causal
claim is not. Measured by deleting the cast and running
pnpm --filter @objectstack/rest typecheck:

src/rest-server.ts(5925,44): error TS2339: Property 'publishMetaItem' does not exist on type 'RestProtocol'.

TS2339 (member does not exist), notTS2353 (unknown key) — the compiler
never reaches the argument. publishMetaItem is an ADR-0076 D9 server-only
extension
: RestProtocol is DataProtocol & MetadataProtocol, and
MetadataProtocol (packages/spec) declares no such member. Only
PublishMetaItemResponseSchema (#7294) exists there — no request schema, no
interface entry. The cast is feature detection, exactly like the auditMetaItem
twin a few hundred lines up.

Re-measured after this PR's fix: byte-identical error. Declaring packageId
on the implementation's request type changes nothing there, because that type
lives in @objectstack/metadata-protocol, which packages/rest deliberately
does not depend on. Removing the cast needs MetadataProtocol to declare the
member plus a PublishMetaItemRequest to hang #9741's TransportScopedMetaRequest
typing off — a packages/spec contract decision, which this card fences out
explicitly. Recorded as a comment at the call site so it is not re-filed; the
decision is raised in the dispatch report.

Evidence

Defect control on the type surface (the runtime path is sound, so this is the
only falsifiable control available — both directions shown):

legtsc --noEmit -p packages/metadata-protocol
baseline (origin/main)63 errors — matches the shrink-only ledger exactly
before-state (pins added, type not yet fixed)65 — two TS2353: ... 'packageId' does not exist in type '{ type: string; name: string; organizationId?: ...; _skipSeedApply?: ... }'
after63 — the packageId errors gone, ledger unmoved

The before-state error prints the declared shape verbatim, independently
reproducing the card's claim about which keys the type carried.

The three new cases are regression PINS, not defect controls — they pass in
the before-state too (7/7 green before the type fix), because the runtime path
was already correct. Stated plainly rather than reported as a green control that
proves nothing.

Ablation — the pins can fail, on the exact mechanism the card names.
Replacing the wholesale forward with a destructuring rebuild that drops
packageId:

× promotes the STATED package draft, not the first row that shares the name
→ expected 'app.other' to be 'app.demo'
× drains the stated package own draft and leaves the other package draft pending
→ expected 'app.demo' to be 'app.other'

That is the #8907 inversion reproduced on the per-item door. The third case
(no package stated) correctly stays green — nothing to drop. The subject resolves
from source (./protocol.js, relative, same package), so the red itself proves
the mutation reached the executed code. Restored and re-verified: git hash-object
== git rev-parse HEAD:PATH, git diff --exit-code 0, ablation marker absent
from srcanddist, suite back to 7/7.

Gates — union derived from the real change set via
node scripts/pm/dispatch-gates.mjs with no path arguments, after the final
commit ca53f9aad5. All 18 path-matched + 5 convention-triggered families run,
exits captured before any pipe, all green. Selected verdict lines:

  • check-engine-double-contract: OK — 377 pinned, 133 in the DEBT ledger, 2 exempt. (no new double — the existing makeStubEngine was reused)
  • check:where-matcherbaseline key set verified against 047ac86: no files added.
  • check:query-options-erasurebaseline key set verified against 047ac86: no files added.
  • check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 332.1s, 1908 raw tsc error(s) total, none above its recorded number.
  • check-nul-bytes: OK (scanned 6350 text file(s) ... no raw ASCII control bytes).
  • OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.

Suites on the final tree: @objectstack/metadata-protocol 1816 passed / 10
skipped (134 files); @objectstack/rest 2179 passed (134 files).

Out of scope, filed

#11003resolveDraftOrgScopeForPublish probes sys_metadata without the
package_id dimension, so now that the per-item door can state a package, the
scope probe and the promote can ask different questions: a package-stating
publish can resolve the wrong org scope and answer no_draft while the named
draft sits publishable in the other scope. Behaviour deliberately untouched here
(#11003 is not addressed by this PR) — it needs its own fixture and a precedence
ruling between the ADR-0005 overlay order and the ADR-0048 package key. The
corrected comment points at it.

Changeset

.changeset/publish-meta-item-declares-package-id.md, patch for
@objectstack/metadata-protocol and @objectstack/rest. Justified: the declared
request type of a published package gains an optional key, which is user-visible
to typed callers — that visibility is the entire point of the card. Additive and
non-breaking, so no ADR-0087 marker is required; check-adr-0087-registration
and check-changeset-no-major both pass.


Generated by Claude Code

os-elonand others added 3 commits August 22, 2026 03:43
…t type
`publishMetaItem` declared `type / name / organizationId / actor / message /
_skipSeedApply` and no `packageId`, while `POST /meta/:type/:name/publish
?package=PKG_ID` (#10063) states one on every HTTP-driven promotion that names
a package. The value already flowed correctly - the request object is forwarded
whole and `canonicalizeMetaRequestType` is a spread - but the binding was
invisible to every typed caller and one destructuring refactor away from being
dropped in silence.
Also corrects three in-tree comments that claimed the per-item door "names no
package", which stopped being true at #10063. The `draftForGate` / `rowToItem`
warning those comments were really defending is preserved verbatim.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
…+ changeset
Measured: deleting the cast yields `TS2339: Property 'publishMetaItem' does not
exist on type 'RestProtocol'`, not a TS2353 about an unknown key. It is ADR-0076
D9 feature detection for a server-only extension `packages/spec` declares no
member for - unchanged by declaring `packageId` on the implementation's request
type. Removing it is a `packages/spec` contract decision; #10350 is not addressed
by this comment beyond recording the measurement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
…ollow-up
The package-dimension asymmetry the corrected comment describes is now tracked
as #11003; behaviour here stays untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019yDEhPBC3tcGkW9bkce1HM
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/rest, touching 9 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx(via meta.publishItem (sdk), publishItem (sdk))
  • content/docs/deployment/validating-metadata.mdx(via publishMetaItem (symbol), publishMetaItem (literal))
  • content/docs/kernel/contracts/metadata-service.mdx(via packageId (literal), /:type/:name/publish (route), /meta/:type/:name/publish (route))
  • content/docs/permissions/authorization.mdx(via packageId (literal))
  • content/docs/permissions/capabilities.mdx(via packageId (literal))
  • content/docs/permissions/permission-sets.mdx(via packageId (literal))
  • content/docs/protocol/kernel/plugin-spec.mdx(via packageId (literal))
  • content/docs/ui/doc-pages.mdx(via packageId (literal))
  • content/docs/ui/setup-app.mdx(via packageId (literal))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via publishMetaItem (symbol), publishMetaItem (literal), /:type/:name/publish (route), /meta/:type/:name/publish (route))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bd003d821787d3d72e68c5297d3d2088a473c5c0packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2f763b5172ce4dff32b9f02d598251a9bee7b8a9 — the merge of head ca53f9aad5a4e989ef284380072c56f2fd362a9d into base bd003d821787d3d72e68c5297d3d2088a473c5c0, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2f763b5172ce4dff32b9f02d598251a9bee7b8a9 && git checkout 2f763b5172ce4dff32b9f02d598251a9bee7b8a9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bd003d821787d3d72e68c5297d3d2088a473c5c0 ca53f9aad5a4e989ef284380072c56f2fd362a9d && git checkout -B drift-repro bd003d821787d3d72e68c5297d3d2088a473c5c0 && git merge --no-ff ca53f9aad5a4e989ef284380072c56f2fd362a9d
node scripts/docs-audit/affected-docs.mjs --json bd003d821787d3d72e68c5297d3d2088a473c5c0

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs bd003d821787d3d72e68c5297d3d2088a473c5c0 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 22, 2026
@os-elon
os-elon marked this pull request as ready for review August 22, 2026 04:17
@os-elon
os-elon enabled auto-merge August 22, 2026 04:17
@os-elon
os-elon added this pull request to the merge queueAug 22, 2026
Merged via the queue into main with commit 490879aAug 22, 2026
32 checks passed
@os-elon
os-elon deleted the claude/issue-10350-publish-request-shape branch August 22, 2026 04:36
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32551602796 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 6.35s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 80 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

publishMetaItem's declared request type omits packageId, and its in-tree comment now states the opposite of what the door does

1 participant

@os-elon