Skip to content

fix(metadata-protocol): thread packageId into both resolveDraftOrgScopeForPublish probes - #11156

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-11003-publish-scope-probe-package-dimension
Aug 22, 2026
Merged

fix(metadata-protocol): thread packageId into both resolveDraftOrgScopeForPublish probes#11156
os-zhuang merged 2 commits into
mainfrom
claude/issue-11003-publish-scope-probe-package-dimension

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#11003

The ruling this implements

Maintainer, 2026-08-22, live session (「同意所有」, item 7 = A), recorded by the triage seat on the issue:

A: the scope probes ask the promote's question (probe with the package) … Scope: thread packageId into both resolveDraftOrgScopeForPublish probes so a package-stating publish resolves the scope of the draft it named. Accepted cost, on the record: a caller stating a package no longer discovers a no-package draft of the same (type,name) — it 404s and the caller retries without ?package=; that narrowing is the ruling, not a side effect.

Options B (package-first with package-agnostic fallback) and C (detect-and-refuse) were closed by the adjudication; B by name — it reintroduces the two-question resolution #8907 removed, and its failure mode silently publishes another package's draft on a mistyped package name instead of failing loudly.

What changed

packages/metadata-protocol/src/protocol.ts, exactly the surface the card fences — the two probes and their single-item caller:

  • resolveDraftOrgScopeForPublish takes an optional packageId: string | null; when present, both sys_metadata probes add package_id to their where, so the scope probe asks the identical question the promote's whereFor asks. undefined spreads nothing — an unstated package keeps the historical package-agnostic probes, matching the promote's own match-any resolution.
  • The caller in publishMetaItem threads the dimension under the same present/absent contract promoteDraftForPublish uses ('packageId' in request, with the ?? null coercion), so all three spellings — absent, null, string — agree between probe and promote by construction.

Accepted cost (the ruling, not a side effect): a publish stating ?package= no longer discovers a package-less draft of the same (type, name) — it answers 404 [no_draft]; the remedy is retrying without ?package=. The changeset (minor per the launch-window convention, with an ADR-0087 not-required (no-migration-prescription) disposition) words that narrowing and its remedy for release-notes readers.

The two pins the adjudication names — both against the real protocol

Both live in protocol-publish-drafts-package-scope.test.ts, the existing ADR-0048 coexistence family, as a new #11003 describe block (5 cases):

  1. The card's coexistence scenario: app.other holds an org1 draft for (object, shared_ticket); app.demo holds an env-wide (organization_id IS NULL) draft for the same (type, name); publish ?package=app.demo with session org org1 finds app.demo's draft. Failing baseline demonstrated: with the probes reverted to origin/main, this case rejects with the card's exact [no_draft] No pending draft exists for object/shared_ticket — probe 1, package-agnostic, matched app.other's org1 row; the package-exact promote then found nothing there.
  2. The inverse shape — the probe lands env-wide because the caller's own org row belongs to another package: pinned row-by-row (the promoted active row is env-wide under app.demo; app.other's org1 draft stays pending, undrained, its partition untouched). Same reverted-probe baseline: rejects [no_draft].

Guard cases beside them, each labelled in-file with whether it had a pre-fix red:

  • No overshoot: when the named package's draft is the caller's own org row (mirrored arrangement), the resolution stays org1 — the ADR-0005 own-org-shadows-env precedence survives the package narrowing. Green pre-fix too, stated so in the test; it bounds the fix, it does not reproduce the defect.
  • Absent key: a publish stating no package keeps fully package-agnostic probes over the cross-scope fixture.
  • Accepted cost, both halves: a package-stating caller 404s (code: 'NO_DRAFT', status: 404) over a package-less draft and the row is untouched; retrying without ?package= publishes it. No pre-fix red (the package-exact promote already answered no_draft here) — it pins the ruling's cost sentence end-to-end.

Fixture notes, per the dispatch's PR #11139 warning: the org-scoped object drafts are seeded through saveMetaItemwith an explicit packageId (a package-stating save — not the package-less save path #11139 rebinds), under the OS_METADATA_WRITABLE=object hatch scoped to the describe (object is allowOrgOverride: false; without the hatch the #6190 gate answers 403 before the probes matter — the hatch is the deployment shape in which the card's 404 is observable). The one package-less draft row is seeded by direct engine.insert, deliberately not through saveMetaItem(mode:'draft'), so PR #11139's binding changes cannot move this fixture's meaning; it carries checksum: hashSpec(body) because the post-promotion drain is an optimistic-lock delete keyed on it. sys-metadata-repository.ts is untouched here; PR #11139 owns that file and is not addressed here.

Verification (at 0e7499df5, sha printed inside the final run)

  • Reverse verification, direction predicted before running: probes reverted to origin/main (mutation proven on disk: fix marker count 0, superseded comment count 1) → the 2 coexistence cases red with [no_draft], all other cases and the rest of the package green (Tests 2 failed | 1862 passed); fix restored (marker count 3, comment count 0) → whole package green.
  • pnpm --filter @objectstack/metadata-protocol test at head 0e7499df5: Test Files 135 passed | 2 skipped, Tests 1864 passed | 10 skipped.
  • Consumer sweep, downstream direction (packages that consume @objectstack/metadata-protocol, run against the rebuilt dist containing the fix): @objectstack/objectql 4034/4034, @objectstack/rest 2192/2192.
  • Full dispatch-gates battery (path-derived + convention-triggered, derived by scripts/pm/dispatch-gates.mjs from the merge base): all 20 green, each exit code captured pre-pipe; highlights in the gates' own words — check-adr-0087-registration accepts the no-migration-prescription disposition; ✓ This diff introduces no 'major' bump; check-engine-double-contract: OK; check-nul-bytes: OK; check-type-check-coverage --re-measure: OK — … none above its recorded number (metadata-protocol is a debt-ledger package with no typecheck script; the ratchet is its type gate).
  • The repo-wide eslint sweep was not run locally and no narrowing is claimed — it is CI-owned.

Generated by Claude Code

…peForPublish probes (#11003)
Maintainer ruling 2026-08-22, option A: the scope probes ask the promote's
question. A package-stating per-item publish now resolves the org scope of
the draft it named; the package-agnostic probe could match another
package's row in the caller's org and answer 404 [no_draft] over a
publishable env-wide draft. Accepted cost, on the record: a caller stating
a package no longer discovers a no-package draft of the same (type, name).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
…less draft row
The post-promotion drain is an optimistic-lock delete keyed on the row's
checksum; a checksum-less hand-built row makes the drain classify the
mismatch as the benign newer-draft race and survive, failing the remedy
half of the accepted-cost pin. Stamp hashSpec(body) exactly as the
repository's put does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 7 documentable anchor(s).

10 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/concepts/metadata-lifecycle.mdx(via ObjectStackProtocolImplementation (symbol))
  • content/docs/deployment/validating-metadata.mdx(via publishMetaItem (symbol))
  • content/docs/kernel/contracts/metadata-service.mdx(via packageId (literal), /: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))

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

  • content/docs/releases/v16.mdx(via ObjectStackProtocolImplementation (symbol))
  • content/docs/releases/v17.mdx(via ObjectStackProtocolImplementation (symbol), publishMetaItem (symbol), /: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 — 7 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 17bad125cca89674cfd2dca59025b0d31da68968packageMentionDocs.

Which tree this was computed on

This run read content/docs from eb8fd3173397b1fc311aa2eafab80eb171e11606 — the merge of head 0e7499df5240d6217589060265b5c4466023f997 into base 17bad125cca89674cfd2dca59025b0d31da68968, 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 eb8fd3173397b1fc311aa2eafab80eb171e11606 && git checkout eb8fd3173397b1fc311aa2eafab80eb171e11606
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 17bad125cca89674cfd2dca59025b0d31da68968 0e7499df5240d6217589060265b5c4466023f997 && git checkout -B drift-repro 17bad125cca89674cfd2dca59025b0d31da68968 && git merge --no-ff 0e7499df5240d6217589060265b5c4466023f997
node scripts/docs-audit/affected-docs.mjs --json 17bad125cca89674cfd2dca59025b0d31da68968

⚠️ 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 17bad125cca89674cfd2dca59025b0d31da68968 → 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-zhuang
os-zhuang marked this pull request as ready for review August 22, 2026 22:18
@os-zhuang
os-zhuang added this pull request to the merge queueAug 22, 2026
Merged via the queue into main with commit c74aefeAug 22, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-11003-publish-scope-probe-package-dimension branch August 22, 2026 22:33
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

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

    ✗ Build failed in 5.79s
    

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

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

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

历史信号:

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

分诊清单:

  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

2 participants

@os-zhuang@claude