Skip to content

fix(metadata-protocol): a publish whose object the runtime registry refused names it under the advisory rule object-registry-refused - #33397

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/area-A4-33005
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/area-A4-33005

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Fixes #33005
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:发布时运行时注册表拒收某个对象,不再只打一行 warn。publishPackageDrafts 在该对象的 published[] 元素上带一条 warning advisory(稳定规则码 object-registry-refused,where 写明对象名),服务端打一次 error 日志(后果 + 修法);publishMetaItem 与 publish 模式的 saveMetaItem 同样带这条 advisory。
  • 为什么改:之前发布答 published/success: true,却没有任何地方点名这个对象,紧接着的种子加载就撞 OBJECT_NOT_FOUND。按裁决选项 D:先告警,outcome 枚举与 success 不变(选项 A 另开卡)。
  • 风险与回滚:低。只在注册表真的拒收时多出一条 advisory 和一行 error 日志;干净发布的响应字节不变,没有改 spec。回滚就是 revert 本 PR。
  • 先看:packages/metadata-protocol/src/protocol-registry-hydration.ts(applyObjectRegistryMutation 改为把拒收交回,新增 reportObjectRegistryRefusal)· protocol-history-and-publish.ts(runPublishSideEffects 与 Phase 2 的元素挂载)· protocol-publish-drafts-advisories.test.ts 新增的 describe。
  • 两分钟验证:pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol-publish-drafts-advisories.test.ts,预期 10/10 通过;其中 Publish: a seed load right after publishPackageDrafts can hit OBJECT_NOT_FOUND for objects the data door serves a minute later — the registry write-through swallows a registerObject failure #33005 两条钉住规则码、对象名与 error 日志只出现一次。
  • 需要你做的:无,席位落地。

What changes

The registry write-through runs after the metadata commit (ADR-0067 D2). When registerObject threw there, ProtocolRegistryHydrationFacet.applyObjectRegistryMutation caught it and logged only console.warn, so publishPackageDrafts answered outcome: 'published', success: true, with nothing naming the object. A seed load right after that publish then hit OBJECT_NOT_FOUND.

This PR implements the maintainer's option D ruling (card comment 6879630490):

  • applyObjectRegistryMutation keeps its warn line and now also returns the refusal (ObjectRegistryRefusal, the thrown value) instead of discarding it. applyRegistryWriteThrough passes it on from its object branch. Every other branch, and every other caller (rollback, revert, orphan adoption, remote mutation, batch registration), behaves as before and ignores the return value.
  • New reportObjectRegistryRefusal(name, refusal) and the stable rule id OBJECT_REGISTRY_REFUSED = 'object-registry-refused'. These follow the pattern of objectStorageSyncRefusedNotice (object-storage-sync-refused). The reporter logs one console.error line that states the consequence and the fix (AGENTS.md "Degradation log levels"), with the registry's reason as a separate argument. It returns one warning in the RuntimeAuthoringIssue shape: where: object "NAME", path: ''. An undeclared failure's text stays out of the 2xx body (clientFacingFailureText).
  • runPublishSideEffects reports the refusal once per call and returns it as registryRefused.
  • publishPackageDrafts Phase 2 appends that advisory to the refused object's published[] element. It comes after the gate's findings and before any storage notice, and an element with no advisories still carries no advisories key. Outcome and success are unchanged.
  • Within the ruling's 30-line allowance, on the existing advisories field: publishMetaItem and a publish-mode saveMetaItem add the same advisory to their own advisories. That is about 8 extra lines.
  • protocol.ts updates the private method signatures of the three host stubs for the new return types. No spec or protocol.zod.ts change; outcome enum untouched.

Tests

New top-level describe in packages/metadata-protocol/src/protocol-publish-drafts-advisories.test.ts, using that file's shared stub harness:

  1. A two-object batch where the registry refuses ticket answers success: true, outcome: 'published', publishedCount: 2. ticket's element carries exactly one advisory: rule: 'object-registry-refused', severity: 'warning', where: 'object "ticket"', the message names 'ticket', the key set matches the other doors, and the undeclared reason is withheld. Exactly one console.error line names ticket, with the reason whole as its second argument. The sibling invoice element has no advisories key.
  2. publishMetaItem and a publish-mode saveMetaItem answer the same rule with the right where, and each logs once.

The test file also gains a module-top import '@objectstack/lint' (required by check:test-source-alias for an edited test file).

Acceptance notes

  • Premise re-measured at origin/main 4c9104e before any edit (scratch test, deleted): {"outcome":"published","success":true,"published":[{"type":"object","name":"ticket","version":"..."}]}, WARN [Protocol] registerObject failed for ticket: registry refused the object, ERROR [], one registerObject call.
  • Overlap: open PRs feat(metadata-protocol): a metadata write's advisories and refusals come back in the caller's language (#32656, 1/2) #33276 and fix(metadata-protocol)!: history and rollback doors refuse a bare two-holder name and address one holder by ?package= #33193 also touch protocol-history-and-publish.ts. This diff's hunks there are one import line, runPublishSideEffects's write-through capture and return type, two lines in publishMetaItem's advisory block, and one line in Phase 2's element loop. It uses the current main advisory helpers and none of their hunks.
  • File surface: protocol.ts (three stub return types) is outside the dispatch's file list, but inside the claim's packages/metadata-protocol/src/**.
  • Not covered by D, left as they were: the object_extension and overlay branches of applyRegistryWriteThrough, and the non-publish callers of the object write-through (rollback, revert, orphan adoption, remote mutation fan-out). They still log at warn and answer nothing. The stricter reading ("never published while an object does not resolve") belongs to the follow-up option A card.
  • Read-only observation: with a real SchemaRegistry, judgeBatchObjectRegistrations registers every promoted object inside restoringOnRefusal in Phase 1, so a refusal that repeats every time already answers refused truthfully. What reaches this advisory is a refusal that shows up only at Phase 2's real registration.

Verification

Final head: 2dfe890ee (objectstack, branch claude/area-A4-33005).

All heavy runs went through scripts/pm/os-verify-lock.sh (slot a4-33005). Exit codes were captured before any pipe.

Package suites
  • pnpm --filter @objectstack/metadata-protocol typecheck && pnpm --filter @objectstack/metadata-protocol test at 2dfe890ee: typecheck clean, Test Files 339 passed | 9 skipped (348), Tests 22846 passed | 43 skipped (22889). tsc --listFilesOnly includes the edited test file.
  • Direct dependents, with each package's dependency closure built: @objectstack/objectql Test Files 595 passed (595), Tests 12946 passed; @objectstack/rest Test Files 410 passed (410), Tests 8370 passed | 332 skipped. Both ran at 474253a15; the only later commit adds one import line to a metadata-protocol test file. The other 80 downstream packages that turbo ls --affected lists are left to CI (Test Core). That set comes from the package dependency graph, so it cannot see tests that read another package's source from disk. No export of the package changed: the new symbols are not re-exported from its index.
  • eslint --no-inline-config on the five touched source and test files: exit 0.
Ablation (one-time proof, not kept as a test)

The fix was committed first. node scripts/ablation-replace.mjs replaced return { error: err }; in applyObjectRegistryMutation with return undefined;; the anchor went 1 to 0 and the blob changed 7f147079c438 to afb04eb6c844. The pin file then failed, Tests 2 failed | 8 passed (10), both failures in the #33005 describe. The tool restored the file: blob back to 7f147079c438, equal to HEAD; git diff HEAD empty; git status --porcelain empty. The test imports ./protocol.js from source, so no rebuild was needed.

Gate families

The families node scripts/pm/dispatch-gates.mjs --commands derives (72) were run one by one. The run at 474253a15 turned up one real finding, check:test-source-alias: the edited test file needed a module-top import '@objectstack/lint'. It was fixed in 2dfe890ee and re-run green. At that point check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt had not been measured (missing built output or a lock wait); all three were re-run at 2dfe890ee and exited 0. The full union is being re-run at 2dfe890ee; the result goes in the os-dev report comment on the card.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cw787kMeaX7oyz7DDC

…efused names it under the advisory rule object-registry-refused and logs it at error once

The registry write-through hands the refusal back instead of keeping it to a
warn line; runPublishSideEffects reports it once at error and returns the
advisory, which publishPackageDrafts puts on the refused object's published[]
element and publishMetaItem and a publish-mode saveMetaItem put on their own
advisories. Outcome and success are unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cw787kMeaX7oyz7DDC
…spelling pin reads

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cw787kMeaX7oyz7DDC
…form at collection

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cw787kMeaX7oyz7DDC
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/kernel/cluster.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), applyRegistryWriteThrough (symbol, a method of class ProtocolRegistryHydrationFacet))

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

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via publishPackageDrafts (symbol, a method of class ProtocolHistoryAndPublishFacet))

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 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 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 7e2eadd2902c3c934c7dcdfaf53206ad41db0b6e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 672a0a4bb0cef66d6553304eb34b4941d7baa754 — the merge of head 2dfe890eee82a22d9850a1125e8c36db2a712b8b into base 7e2eadd2902c3c934c7dcdfaf53206ad41db0b6e, 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 672a0a4bb0cef66d6553304eb34b4941d7baa754 && git checkout 672a0a4bb0cef66d6553304eb34b4941d7baa754
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7e2eadd2902c3c934c7dcdfaf53206ad41db0b6e 2dfe890eee82a22d9850a1125e8c36db2a712b8b && git checkout -B drift-repro 7e2eadd2902c3c934c7dcdfaf53206ad41db0b6e && git merge --no-ff 2dfe890eee82a22d9850a1125e8c36db2a712b8b

node scripts/docs-audit/affected-docs.mjs --json 7e2eadd2902c3c934c7dcdfaf53206ad41db0b6e

⚠️ 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 7e2eadd2902c3c934c7dcdfaf53206ad41db0b6e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cw787kMeaX7oyz7DDC

Request: review · area A4 · head 2dfe890

Fixes #33005 (maintainer ruling D, 6879630490) · metadata-protocol, 6 files, +208/−11 · NOT governed. Declared Clause-② no; please judge whether the new advisory rule code makes it yes (widening) as on #33336.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4cxDxAYxdtDhtR1kQQe · head 2dfe890 · 2026-11-14T18:52Z

review#2 (server area pilot, A4).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Review: PASS

Head: 2dfe890eee82a22d9850a1125e8c36db2a712b8b · reviewer review#2 (session_016WV4cxDxAYxdtDhtR1kQQe) · area A4 · Fixes #33005

Checked against the maintainer ruling (6879630490, option D):

  • Advisory on the element a client reads. applyObjectRegistryMutation now answers the refusal from its existing catch, the one place a registerObject refusal was kept to a warn line, and undefined on every other path. applyRegistryWriteThrough passes it on for the object branch only. runPublishSideEffects turns it into one RuntimeAuthoringIssue (object-registry-refused, warning, where: object "NAME"). Then publishPackageDrafts appends it to that object's published[] element, ahead of the storage notice, and publishMetaItem and publish-mode saveMetaItem append it to their own advisories. The outcome, success and the counts are unchanged, and a clean answer adds no key (pinned: the sibling element's keys stay name, type, version).
  • Logged once at error. There is one reportObjectRegistryRefusal call per refused object per write, and the test counts exactly one line for each door. The registry's text reaches the caller only through clientFacingFailureText, as on object-storage-sync-refused. The test pins that the undeclared text stays out of the 2xx body.
  • Stable rule code for the follow-up card (A) and for cloud: OBJECT_REGISTRY_REFUSED = 'object-registry-refused'.
  • Clause-②: no accepted. Here is why it differs from feat(lint): warn on a multi update_record that moves a stored expression input #33336: there, the deciding fact was the new rule id exported from the @objectstack/lint root. Here OBJECT_REGISTRY_REFUSED, ObjectRegistryRefusal and reportObjectRegistryRefusal are not re-exported from packages/metadata-protocol/src/index.ts (the package's only exports entry), and the change in the answer is one more element in the existing optional advisories. That is the same shape as fix(metadata-protocol): an object save or publish whose storage sync is refused says so on its answer #30691 and fix(metadata-protocol): report a refused object-storage sync on the batch publish element #30754 (object-storage-sync-refused), both declared and landed Clause-②: no. Metadata-protocol: none is right: what an author can write does not move.
  • CI on this head: 35 success, 16 skipped, none failed.

Nits (optional, no card owed):


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cw787kMeaX7oyz7DDC

ACCEPT · area A4 · head 2dfe890 · 2026-11-14T18:54Z

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 18:55
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit bd439f1 Nov 14, 2026
52 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/area-A4-33005 branch November 14, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants