Repository navigation
feat(client): meta.publishItem names the package whose draft it promotes, sent as ?package= - #23680
Conversation
…tes, sent as ?package= A per-item publish that names no package is refused 409 RESOURCE_CONFLICT for an item two or more packages hold, and 404 NO_DRAFT when the item's one package holds no draft and another package's draft is stored; both refusals prescribe ?package=. publishItem gains opts.packageId, sent as ?package= with metaSaveQuery's guard: only a non-empty id reaches the wire. message still rides the body. Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
… real publish door Wire rows over a fake fetch: packageId reaches the publish URL as ?package=, undefined and '' send nothing, message still rides the body. Real-door rows over RestServer, the real protocol and SqliteWasmDriver: a publish naming no package of an item two packages hold is refused 409 RESOURCE_CONFLICT, and naming one promotes that package's draft only; an item whose only draft is another package's is refused 404 NO_DRAFT, and naming the draft's package promotes it. Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
Contract reviewServed-tier: The review is owed by the It rests on:
Nothing was built or run. ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对✅ ACCEPT: PR #23680 at
Checklist, read against GitHub and
Accepted deviations:
Out-of-scope, Acceptance notes, 「承接者:无」: the dispatcher catch-all serves no Landing: Generated by Claude Code |
Fixes #23658
Clause-②: yes (widening)
What this does
client.meta.publishItem(type, name, opts?)takesopts.packageId?: string, sent as?package=IDonPOST /api/v1/meta/:type/:name/publish.undefinedand''send nopackagekey. This ismetaSaveQuery's guard, withURLSearchParams.set(neverappend, because the door refuses a repeated?package=).messagestill rides the body, unchanged. An options-less publish sends byte-identical requests to before.deleteItem's does..changeset/23658-client-publish-package.md:@objectstack/clientminor,feat(client):, in client:meta.deleteItemcannot name a package — after #23165 a delete of a name two packages hold answers 409, and the SDK has no way to pass the?package=remedy #23293's house form.Why
Since PR #23654 (
57994c311f), a per-item publish that names no package is refused with a?package=remedy, and nothing is published:409 RESOURCE_CONFLICTfor an item two or more packages hold;404 NO_DRAFTwhen the item's one package holds no draft and another package's draft is stored.The SDK could not send the remedy. This is the publish half of what #23293 did for
deleteItem.Measured first: the two mechanism hypotheses
(a) One declaration: holds.
publishItemis declared once, on the unscopedObjectStackClient.meta(index.ts, themetaobject at:1812). The environment-scopedclient.environment(id).meta(:7698on the base) declaresgetTypes,getItems,getItem,saveItem,deleteItemandgetHistory, and nopublishItem. No method is added there. A wire row pins the absence, withdeleteItemon the same object as its positive control.(b) The doors read
?package=: the REST door does, and the dispatcher serves no per-item publish door.metaItemPackageBinding(req.query?.package)(the save and delete doors' function) and spreadspackageIdintopublishMetaItem's request. The real-door rows below drive this door.createHonoAppcatch-all): measured on the runtimedist/built at57994c311f.HttpDispatcher.handleMetadata('/view/x/publish', ctx, 'POST', {}, { package: 'pkg' })answers404 ROUTE_NOT_FOUND(Route Not Found: /meta/view/x/publish), and the spypublishMetaItemis called 0 times.PUT /view/x?package=pkganswers 200, andsaveMetaItemreceivespackageId: "pkg".createMetaDomain's handler andhandleMetadataboth callhandleMetadataRequest, so this is the body the catch-all reaches past its gates.RestServer-owned route (rest-route-ledger.ts,source: 'route-manager'), and the runtime route ledger has no row for it. So the?package=forwarding question does not arise on the dispatcher. See Acceptance notes.dispatch()leg. My probe got401 UNAUTHENTICATEDthere even with identity stubbed, and thePUTcontrol got the same 401, so that leg never reached the domain.Pins:
packages/client/src/meta-publish-item-package.test.ts(new file)A new file rather than beside the twin. The twin's real-door rig is local to
meta-delete-item-carriers.test.ts, and importing it from that test file would register every suite of that file a second time. A helper module undersrc/would sit in the build program, which excludes only*.test.ts. So the rig is copied here, narrowed to the publish door. It imports exactly the workspace packages the twin imports, socheck:test-source-alias's@objectstack/clientrow is unchanged.Wire rows (a fake
fetch):packageId: 'pkg'sends?package=pkgon the publish URL, and the body names no package;undefined,'',{}and no options send no query;messagestill rides the body, with and without a package, and the key is sent once;saveItemanddeleteItem;metadeclares nopublishItem.The real door (
RestServerover the realObjectStackProtocolImplementationonSqliteWasmDriver, the twin's rig; the only stub is the auth boundary; rows read straight out ofsys_metadataaspackage:state:label):com.qa.alphaandcom.qa.beta, plus a pendingcom.qa.alphadraft. RefusedRESOURCE_CONFLICT/ 409, the rows are unchanged, and nopackageIdreached the protocol.{ packageId: 'com.qa.alpha', message }succeeds.com.qa.alpha's draft is now its active row,com.qa.beta's row is untouched, and the protocol receivedpackageIdandmessage.com.qa.alphawhose only draft iscom.qa.beta's. Naming none is refusedNO_DRAFT/ 404 and the rows are unchanged;{ packageId: 'com.qa.beta' }succeeds, and that draft becomescom.qa.beta's active row.Ablation (one-shot, at
0405b4f4cd)node scripts/ablation-replace.mjsreplaced theparams.set('package', …)line with a marker. On-disk proof: anchor 1 to 0; marker 0 to 1; set-line count 2 to 1 (getDiagnosticskeeps its own); blob6fcd25dtoc5a8538.trap … EXIT INT TERMthat restores fromHEADon an absolute path../index(source), not throughexports.?package=wire row; themessagerow (expected '' to be '?package=com.qa.alpha'); the encoding row; two packages naming one (refusedRESOURCE_CONFLICT/ 409 by the door); the NO_DRAFT row (refusedNO_DRAFT/ 404).6fcd25d, equal to HEAD's;git diff HEADand porcelain are empty.Verification
All build and test runs went through
scripts/pm/os-verify-lock.sh, withNODE_OPTIONS=--max-old-space-size=4096.At
0405b4f4cd:turbo run build --filter=@objectstack/client^...: 33/33.pnpm --filter @objectstack/client typecheck: exit 0 (tsc --noEmit, pluscheck:test-typecheck: OK, 0 errors).meta-publish-item-package.test.ts,client.test.ts): 2 files, 220 passed.mainmerged in atdbdcf90560(origin/maina4b87ef556; the incoming overlap ismetadata-protocol'sprotocol.ts, anobject-type read filter, not the publish path):turbo run build --filter=@objectstack/client...: 34/34.pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date.Lint:
pnpm lintis narrowed to eslint over the two changed TypeScript files, with three pieces of evidence:eslint --no-inline-configon both files: exit 0;--print-configshows the repo config applies (6 and 5 rules on; the files are not ignored);--stdin(a restricted@objectstack/specroot import appended to the new file) exits 1 withno-restricted-imports.CI's
lint-eslintlane runs the fullpnpm lint.Gates (local, at
dbdcf90560)Derivation:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 63 commands atdbdcf90560.Run: all 63, each exit code captured before any pipe. Four roster families whose roster sits in one of this diff's directories ran too:
check-changeset-fixed,check:authz-resolver,check:error-code-casingandcheck:filter-alias-parity.check:type-check-debtfirst refused with exit 3: its closure build got no lock turn, so nothing was measured. I re-ran it whole under the lock: OK, 26 raw tsc errors, none above the recorded number.--ran: 63 derived, 63 run, 0 NOT MEASURED, 0 unrun.Verdict lines:
check-adr-0087-registration: no declared-breaking changeset (1 non-breaking seen).check-changeset-no-major,check-empty-changeset: pass.check-changeset-fixed: 72 public packages in sync.check:test-source-alias,check:cross-package-test-inputs: OK.check:published-files: pass.check:dts-closure: 120/120 declaration files present.check:nul-bytes: OK.check:skill-examples: 263 prose examples type-check.check:dual-build-cjs-loads: pass.At
0405b4f4cd: the same 63 ran, 61 exit 0 and 2 exit 3 (PREREQUISITE NOT MET):check:skill-exampleswantedclient-react'sdist/, andcheck:dual-build-cjs-loadsmet unbuilt siblings. Both measured green atdbdcf90560.Newer
main:mainmoved 2 commits afterdbdcf90560, to8d1fb1fb2d. Those are 22 paths, none inpackages/client; the derivation namesscripts/check-route-envelope.mjsas changed there. CI runs on the merge ref.CI-owned, not re-run locally: the path-scheduled jobs (Test Core, Build Core, the dogfood jobs, Temporal Conformance), the type-check lanes, the artifact-roster families and the full
pnpm lint. Their check-runs are the verdict.Acceptance notes
createHonoApp's dispatcher catch-all, withoutRestServer,client.meta.publishItemanswers404 ROUTE_NOT_FOUNDwhatever the package. The runtime route ledger has noDELETE /meta/:type/:namerow either, sodeleteItemlands on the same tail.codeand status, the stored rows and the protocol probe, not the door's sentences, because no consumer parses that prose.Generated by Claude Code