Repository navigation
fix(metadata-protocol): org overlay withdrawal and publish gate follow-ups (package identity, judged draft, lock key, row anchor) - #21962
Conversation
…every name, so the form doors judge every package's withdrawal The view list read serves one item per package that ships a view name (ADR-0048) whether or not a view row is stored; only a name a stored container's expansion writes is upserted by name. The env-wide list is the layer the anonymous form doors judge a withdrawal against, so a package-less organization overlay stored before one package's withdrawal is compared against every package's body of the name. Pin: a package-less organization overlay under two packages shipping its view name is served once per package, each copy stamped with that package; after either package withdraws the name env-wide (first or second in registry order), the env-wide list holds the withdrawal, the doors serve no copy, and a re-save of the overlay is refused. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…dged The publish gate reads the draft to judge it, and the promotion read the draft row again to write it. The promotion is now handed the judged draft's hash (null when the gate found no draft): SysMetadataRepository.promoteDraft takes an optional expectedDraftHash and refuses a draft row with another hash, or any draft row where the caller judged none, with a ConflictError before anything is written. The protocol answers it as 409 METADATA_CONFLICT with its own wording. Pin: a draft saved after the gate read, or where the gate judged none, is not promoted and the conflict answers; with no save in between the judged draft is promoted and its draft row drained. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…y it resolved The publish path resolves one package key for the draft it promotes (the caller's stated binding, else the draft row's own) and reads and promotes the draft under it. Its ADR-0010 lock lookup took only the package the request stated. The key is now resolved first and threaded into the lock lookup too. The authoring gate's narrowing to the stated package is left as it is. Pin: with two packages' env-wide rows of one view both locked, a publish that states no package is refused with the lock of the draft row's own package; stating a package consults that package's lock. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…that package's env-wide definition The organization-scoped save check judges a view overlay against the env-wide body of the row it is keyed by. That anchor is now resolved per package, the way the list read resolves each package's item: the package's own env-wide row, else the package-less env-wide row (which stands in for every package), else that package's artifact. It no longer falls back to one artifact per name, the first in registry order, and one package's stored row no longer stands for every package's artifact of the name. The "never under-closes" wording on the withdrawal judgement's docblock and on the public data collection page is narrowed to match what holds: a withdrawal of a name may over-close across packages; both checks read every package's env-wide definition, except that the anonymous form endpoints read one package's expansion of each form name when several packages' stored view containers expand it. Pin: with the withdrawing package not first in registry order, a package-less or package-bound org save that renames the form is refused; another package's env-wide row anchors that package only; controls: the save that keeps the form withdrawn saves, and a package-less env-wide row stands in for every package. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
…g-overlay-publish-gate
📓 Docs Drift CheckThis PR changes 2 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cac27bf66ae2c9454c2d8ff2e31bdcc1eb5ec708 && git checkout cac27bf66ae2c9454c2d8ff2e31bdcc1eb5ec708
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2a22177ae786b1adf6cc22ce596c47bddd254b4a 3eea8f0995e4348d93f8deac466698fe852b17aa && git checkout -B drift-repro 2a22177ae786b1adf6cc22ce596c47bddd254b4a && git merge --no-ff 3eea8f0995e4348d93f8deac466698fe852b17aa
node scripts/docs-audit/affected-docs.mjs --json 2a22177ae786b1adf6cc22ce596c47bddd254b4a
|
…dable store as the lock read did Item 3 moved the publish path's draft-key read ahead of the lock check. An unreadable store is now answered at that read the way the lock read answers it: an unprovisioned sys_metadata holds no draft, and any other failure is the 503 SERVICE_UNAVAILABLE the lock read raised before, never the driver's own error. Pin: a publish that states no package, over a store that cannot be read, answers 503 and promotes nothing. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #21934 body; triage grade 6007709051; addendum 6008978556 (item 4 in scope); unlock 6009643967; claim 6010039236 (revised in place to ① Derived judgmentsKill-switch invariant across the four items: right in code. Each change refuses a superset of what base refused at the place the card names, and opens nothing base closed. The only wrong derived judgment is in the wording deliverable the addendum asked for (last bullet of this section). Item 1, the view list keeps one item per package (
Item 2, a publish promotes only the draft its gate judged.
Item 3, the lock lookup takes the key the gate resolved.
Item 4, the save check anchors each package's row on that package's env-wide definition (
Wording deliverable (addendum 6008978556: "narrow that sentence to match"): WRONG, blocking. Three places state the endpoint exception as "when two packages each have an environment-wide copy of the same view container saved" ( Check-runs on this head: right. Test Core (all 6 shards), TypeScript Type Check, the four Type Check gates, Lint and Repo Gates, Check Changeset, Build Core, Build Docs, Check Documentation Links, Dogfood Regression Gate (3 shards), Dogfood Verify CLI, Temporal Conformance, Spec property liveness, Governed Surface Queue Guard and the PR-context guards all conclude success; Console Pin Gate and the opt-in packed-tarball smoke are skipped. Their conclusions are the gate verdicts; nothing was re-run. ② Semver level
③ Boundary flagsDev report 6011280365, each flag answered:
Implemented-by: VERDICT: FAIL Blocking defect, one: the narrowed "Known limit" wording ( Adopted by REWORK, patch round 1 (narrow). The code of all four items stands. Only the residual's wording changes, in the three places the record names, plus the PR body:
The exception, stated as the record directs: where a package's environment-wide copy of a view container is saved, the endpoints read that copy's expansion alone for each form it expands, and can miss another package's withdrawal of that form, whether saved or shipped. Option A stays with #21967. Its reach is corrected there in this round. Generated by Claude Code |
…nv-wide container copy, and shipped withdrawals too The narrowed "Known limit: packages and names" wording stated the anonymous endpoints' exception as two packages each saving an env-wide copy of one view container. One saved copy is enough: where a package's env-wide copy of a view container is saved, the endpoints read that copy's expansion alone for each form it expands, and can miss another package's withdrawal of that form, whether saved or shipped. The organization-scoped save check still judges every package's env-wide definition of the name, and a withdrawal of a view name still closes that name in every package, so it may over-close. Corrected in the public data collection docs page, this card's save-check changeset and the anonymousFormIntakeWithdrawnIn docblock. Reading each package's expansion separately is tracked in #21967. No code change. Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Round 2, after the FAIL record on ① Derived judgmentsWhat moved: wording only. Items 1 to 4, code (unchanged since The blocking defect of round 1: cured. The three sentences now state the endpoint exception at the reach the code has:
The round's new remedy sentence ("To close such a form at the endpoints, withdraw it in every saved environment-wide copy of that container as well", Readers and accept sets: unchanged from round 1. No code moved, so the reader enumeration and the "no accept set narrows" judgment of the round-1 record hold. Check-runs on this head: right. Test Core (6 shards), TypeScript Type Check, the four Type Check gates, Lint and Repo Gates, Check Changeset, Build Core, Build Docs, Check Documentation Links, Flag docs affected by code changes, Dogfood Regression Gate (3 shards), Dogfood Verify CLI, Temporal Conformance, Spec property liveness, Governed Surface Queue Guard and the PR-context guards conclude success. Console Pin Gate and the opt-in packed-tarball smoke are skipped; the duplicated Auto Label and Check PR Size entries are re-runs on the body patch, one skipped and one success each. None failed. Their conclusions are the gate verdicts; nothing was re-run. ② Semver level
③ Boundary flagsDev round report 6012226132:
Round-1 flags, closed out:
Implemented-by: VERDICT: PASS Adopted by ACCEPT (seat review). The seat read the round's diff against
Generated by Claude Code |
Fixes #21934
Clause-②: yes (widening)
Four LOW/INFO follow-ups to the public-form withdrawal work of #21864, one commit and one pin each, so any item can be dropped at review without the others. Each change is described in the card's public terms. All four land in
@objectstack/metadata-protocol; the only other source edit is a docblock in@objectstack/metadata-core, plus the narrowed sentence on the public data collection docs page.Item 1: package identity of a served org overlay (
21f75eb892)Measured. The judgement the anonymous form doors and the organization-scoped save check share (
anonymousFormIntakeWithdrawnIn,packages/metadata-core/src/anonymous-form-intake.ts:329) compares no package, and the doors' lookup (findPublicFormView,packages/rest/src/rest-server.ts:10735) reads no_packageId. So the package stamp the list merge puts on a package-less org overlay (packages/metadata-protocol/src/protocol.ts:2166and:9077) cannot by itself make a withdrawal miss it. The item's outcome was still reachable onmain(a3bd157730), through the same list merge rather than through a package comparison: the env-wide view list the doors judge against could hold only one package's item of a view name that two packages ship. Measured through the protocol's real list reads and the doors' own verdict: an overlay stored package-less before the withdrawal stayed open after one package's withdrawal and closed after the other's.Changed.
protocol.ts, the list merge's view branch: only a name a stored view container's expansion writes is upserted by name. Every other name keeps one item per package that ships it (ADR-0048), as the list already served it while no view row was stored. Neither of the card's two directions applies (nothing compares packages, so marking stamped copies or reading the org row's ownpackage_idchanges no verdict); the fix is at the producer of the layer the doors read. No door code changes.Pin (
protocol.org-scoped-write-refused.test.ts, "a package-less organization overlay, two packages shipping its view name"): the organization read serves the overlay once per package, each copy stamped with that package; for the package first and the package second in registry order, after it withdraws the name env-wide the env-wide list holds the withdrawal beside the other package's body, the doors serve no copy of the overlay, and a re-save of the overlay is refused.Item 2: the publish gate and the promotion are separate reads (
d1365db627)Measured (H2 confirmed).
promoteDraftForPublishreads the draft throughrepo.getto judge it (protocol.ts:21623at base), andSysMetadataRepository.promoteDraftreads the draft row again with its ownfindOne(sys-metadata-repository.ts:969at base). Nothing tied the two reads together, so a draft saved between them, or a draft that appeared where the gate found none, was promoted without being judged.Changed. A publish promotes only the draft its gate judged.
SysMetadataRepository.promoteDrafttakes an optionalexpectedDraftHash(string | null): when stated, the draft row it reads must carry that hash (withnull, no draft row may exist), otherwise it throws aConflictErrorsubclass before anything is written.promoteDraftForPublishpasses the judged draft's hash (ornull), and answers the conflict as409 METADATA_CONFLICTwith its own wording (publish again to judge and promote the current draft). This coverspublishMetaItemand each promotion ofpublishPackageDrafts.A route without a new public option exists and was not taken: the existing
deriveActiveBodycallback receives the body the promotion read and could compare it with the judged body and throw. It turns a derivation hook into a guard and compares bodies instead of the stored hash the card's direction names, so the explicit option was preferred. That option is the Clause-② widening below.Pin ("a publish promotes only the draft its gate judged"): a draft saved after the gate read, and a draft saved where the gate judged none, are not promoted and the conflict answers; control: with no save in between, the judged draft is promoted and its draft row drained.
Item 3: the lock lookup uses the request's package (
114ed6393c, follow-up7c30229b43)Measured (H3 confirmed). The publish path passed
request.packageIdtolockWriteRefusal(protocol.ts:21583at base), while the gate resolves its draft key a few lines later: the stated binding, else the resolved draft row's ownpackage_id(draftKey). Since the lock resolution reads every row and every shipping package in scope and takes the strictest lock, the package in the address decides whose lock prose the refusal carries, not whether it refuses: the INFO grade.Changed. The draft key is resolved before the lock check and threaded into the lock lookup. The authoring-rule narrowing to the stated package is left exactly as it is. Follow-up
7c30229b43: with the draft-key read moved above the lock check, a store that cannot be read is answered at that read as the lock read answered it before (an unprovisionedsys_metadataholds no draft; any other failure is503 SERVICE_UNAVAILABLE, never the driver's own error).Pin ("a publish consults the lock of the package key it resolved"): with two packages' env-wide rows of one view both locked, a publish that states no package is refused with the lock of the draft row's own package; control: stating a package consults that package's lock. Follow-up pin ("a publish that states no package, over a store that cannot be read"): it answers 503 and promotes nothing.
Item 4: the save check's row anchor across packages (
1e271aaae1)Measured (H4 confirmed).
envWideRawViewRows(protocol.ts:16092at base) returned every stored env-wide row of the name when any existed (so one package's row hid every package's artifact), and otherwise fell back tolookupArtifactItem(type, name)with no package key (the first package in registry order).Changed. The save check anchors each package's row on that package's env-wide definition: the package's own env-wide row, else the package-less env-wide row (which stands in for every package, as in the list merge), else that package's artifact, read through
shippedArtifactsOf.Wording. The "never under-closes" sentence is narrowed in the
anonymousFormIntakeWithdrawnIndocblock and in the "Known limit: packages and names" paragraph ofcontent/docs/ui/public-data-collection.mdx(declared todomain:devxon #6023). The released changeset of #21864 is not edited; this card's changeset states the narrowing. As corrected in3eea8f0995after the contract review, the narrowed text keeps "a withdrawal of a view name still closes that name in every package, so it may over-close" and the statement that the organization-scoped save check judges every package's environment-wide definition of the name, and states the endpoints' one exception: where a package's environment-wide copy of a view container is saved, the endpoints read that copy's expansion alone for each form it expands, and can miss another package's withdrawal of that form, whether saved or shipped. Reading each package's expansion separately is tracked in #21967. The narrowed text assumes items 1 and 4 both land; if item 1 is dropped, the endpoint exception in that paragraph widens to every view name two packages ship.Pin ("the save check anchors each package's row on that package's env-wide definition"): with the withdrawing package not first in registry order, a package-less and a package-bound org save that renames the form are refused; another package's env-wide row anchors that package only; controls: the save that keeps the form withdrawn saves, and a package-less env-wide row stands in for every package.
Clause-②
Measured against the built entry declarations, base
a3bd157730against head5297072f13, comments stripped before the diff:@objectstack/metadata-protocoldist/index.d.ts:SysMetadataRepository.promoteDraft(ref: MetaRef, opts: {...})gainsexpectedDraftHash?: string | null;(head line 9883). An optional input field: a widening. The only other declaration difference is comment placement.@objectstack/metadata-coredist/index.d.ts: the declaration ofanonymousFormIntakeWithdrawnIn(parameterslayer,view,candidate, returningboolean) is byte-identical (base line 21311, head line 21316); only its docblock changed.Unchanged at the final head
3eea8f0995: the later commits change a method body, a docblock, the docs page and a changeset, and the rebuilt declarations are identical with comments stripped. SoClause-②: yes (widening), and item 2's changeset isminor. The other three changesets arepatch.@objectstack/metadata-corecarries no changeset: its edit is a comment.Tests
Final head
3eea8f0995(origin/main76fec88b16merged at5297072f13). The last commit,3eea8f0995, corrects wording only (the docs page, one changeset, a docblock);packages/metadata-protocol/srcis byte-identical at7c30229b43, where its suites ran:@objectstack/metadata-protocolat7c30229b43: typecheck green (tsc --noEmit; the edited test file is in the program, counted with--listFiles), full suite 218 files passed, 3 skipped; 27984 tests passed, 19 skipped.@objectstack/metadata-coreat3eea8f0995: typecheck green (both programs); 18 files, 411 tests passed.@objectstack/objectql(a consumer of the protocol, against itsdistbuilt at5297072f13; the later code commit only changes an outage path): 378 files, 7507 tests passed.scripts/ablation-replace.mjs, each from a committed head, each item's code set back to its base shape (anchor hit once, blob changed on disk; the test imports the source, so nodistleg), the item's pin run, then restored and proved (blob equal to HEAD,git diff HEADempty):1e271aaae1(itsprotocol.tsblob is the one at5297072f13): item 1: 5 red, 2 green (the two write-door re-save cases, which item 1 does not touch); item 2: 2 red, 1 green (the control); item 3: 1 red, 1 green (the control); item 4: 3 red, 2 green (the two controls);7c30229b43: item 3's follow-up, its store-failure classification removed: its pin 1 red.3eea8f0995, derived bynode scripts/pm/dispatch-gates.mjs --commands(no paths; the same 93 commands as at5297072f13and7c30229b43): 92 run green, among themcheck:doc-authoring,check:docs-audit-scope, the docs-auditcheck-affected-docsandcheck-drift-comment,check:docs,check:nul-bytes, and the changeset gates (check-changeset-no-majorwith this PR's payload,check-empty-changeset,check-adr-0087-registration,check:changeset-gate-self-tests). 1 NOT MEASURED:pnpm check:dual-build-cjs-loads(PREREQUISITE NOT MET: it loads every workspace package'sdist, 32 of which were not built in this worktree; it was green at7c30229b43, whose code this head keeps). Reconciled:dispatch-gates --rananswers "93 derived famil(ies) accounted for — 92 run, 1 NOT-MEASURED". The artifact-roster block (53) is green, the PR-context gates run against this PR. The four symbol-anchor sweeps (check:adr-symbol-anchors,check:scripts-symbol-anchors,check:spec-docblock-symbol-anchors,check:adr-anchors) are green.Acceptance notes
domain:clion [PM seat] domain:cli — 🟢 os-warren · session_01RWZbGvPFcRKvUqASZtunCU #6024).@objectstack/metadata-protocol(not exported from its entry); callers see aConflictError.Generated by Claude Code