Repository navigation
[finding] POST /meta/:type/:name/publish is a served REST route with NO spec declaration — the #5745 "declared = returned" discipline covers only the save door #7294
Description
Activity
CLAIM — spec-lane PM seat (#6017), session
session_01PiRUoQkTSBBmpyXBY3cVn2. Branch:claude/issue-7294-declare-publish-response. Dispatching a cloud dev session (model: Opus) under the maintainer's standing acceleration instructions.Direction: mirror the #5745 discipline one door over, exactly as the card lays out — the publish door gets the same three artifacts the save door has: (1)
PublishMetaItemResponseSchemainpackages/spec/src/api/protocol.zod.tsdeclaring the full measured body (success/version/seq/message+ conditionalseedApplied/materializeApplied/projectionApplied, fromprotocol.ts:9771-9798); (2) the endpoint declaration inplugin-rest-api.zod.ts(POST {metaPath}/:type/:name/publish,handler: 'publishMetaItem',responseSchemareference), matching the 6 existing metadata-category entries' shape; (3) a producer-side conformance gate mirroringsave-meta-response-conformance.test.tsincluding its header discipline. Premise-first: re-measure that the route is still served and still undeclared. New public exports expected ⇒ dual-snapshot rule in full.
Generated by Claude Code
OS-DEV-REPORT
{ "issue": 7294, "outcome": "success", "branch": "claude/issue-7294-declare-publish-response", "pr_number": 7356, "premise_check": { "base": "origin/main @ 88154be", "route_still_served": true, "route_evidence": "rest-server.ts:5481 registers POST {metaPath}/:type/:name/publish; :5499 awaits publishMetaItem(...); :5504 res.json(result) verbatim", "response_body_still_as_measured": true, "body_evidence": "protocol.ts:9769-9798 — one response literal: success/version/seq/message, then seedApplied/materializeApplied/projectionApplied attached conditionally by runPublishSideEffects", "publishmetaitem_hits_under_packages_spec_src": 0, "publish_hits_in_plugin_rest_api_zod": 0, "single_producer_confirmed": true, "verdict": "premise holds in full — not already declared; proceeded" }, "changes": [ "packages/spec/src/api/protocol.zod.ts — PublishMetaItemResponseSchema (full measured body; success/version/seq required, message + seedApplied/materializeApplied/projectionApplied optional) + PublishMetaItemResponse alias, modelled field-for-field on SaveMetaItemResponseSchema", "packages/spec/src/api/plugin-rest-api.zod.ts — endpoint entry POST /:type/:name/publish, handler publishMetaItem, category metadata, responseSchema PublishMetaItemResponseSchema, permissions ['metadata.write']; publishMetaItem added to the group's methods", "packages/objectql/src/publish-meta-response-conformance.test.ts — NEW producer-side gate mirroring save-meta-response-conformance.test.ts incl. header discipline; 9 cases covering plain shape + each conditional receipt path", "packages/spec/src/api/protocol.test.ts — spec-side half the gate's header points at; 8 cases pinning what the schema says", "packages/client/src/index.ts — publishItem() typed Promise<PublishMetaItemResponse> (was any); type re-exported", "packages/metadata-protocol/src/protocol.ts — publishMetaItem's Promise<...> annotation gains projectionApplied (assigned in the body, missing from the type)", "packages/spec/src/type-alias-convention.pin.test.ts — ADR-0122 pin Iso836; count 824 -> 825 + both prose statements", "packages/spec/src/api/plugin-rest-api.test.ts — metadata endpoint count 5 -> 6 with the receipt", ".changeset/declare-publish-meta-item-response.md — spec: minor, metadata-protocol + client: patch" ], "judgement_calls": { "message_optional_not_required": "producer sets it unconditionally today, but its own signature says message?, the SaveMetaItemResponse twin is optional, and an optional-but-always-present field strips nothing — stripping is the #5745 failure mode. success/version/seq ARE required (sole producer's only success return always sets them), with the tripwire case in the gate.", "no_request_schema": "#3899 — body optional, only read key is `message` taken only when already a string; the route cannot 400 a malformed body, so a requestSchema would advertise a gate that does not run." }, "client_reexport_decision": { "included": true, "why": "mirror is exact and cheap — publishItem() already existed and already returned unwrapResponse<any>, the concrete symptom the card named. One type import + one re-export line, both beside the SaveMetaItemResponse ones. No extra surface pulled in; client suite 279/279 green, tsc --noEmit clean." }, "reverse_verification": { "direction_predicted_first": true, "prediction": "removing the seedApplied block from the schema turns the producer gate RED in exactly 2 cases (the two seed paths) via non-empty strippedKeys; the other 7 stay green; the save-door gate is untouched", "probe": "omission — deleted the seedApplied z.object block from PublishMetaItemResponseSchema", "result_matched_prediction": true, "observed_red": "publish gate 2 failed / 7 passed, both failures `expected [ 'seedApplied' ] to deeply equal []` at the strippedKeys lines; spec-side protocol.test.ts 2 failed (the two seedApplied cases); save-meta-response-conformance.test.ts 5/5 untouched", "observed_green_as_landed": "publish gate 9/9, protocol.test.ts 44/44, save gate 5/5", "stronger_than_predicted": "`pnpm --filter @objectstack/spec build` itself REFUSES the removal before any test runs — '1 authorable key(s) disappeared from the contract: api/PublishMetaItemResponse:seedApplied'. The declaration is covered by the authorable-surface gate too, not only the two suites.", "restored": "probe reverted from backup; schema file byte-identical to pre-probe state; spec rebuilt and all suites re-run green" }, "gates": { "spec_build_before_dist_derived_regen": "yes — #7122 trap hit once in the honest direction: gen:api-surface REFUSED ('dist older than src') after a post-build source edit, was re-run after a rebuild", "check_generated": "all 11 artifacts up to date", "typecheck": "turbo typecheck spec/client/objectql/metadata-protocol/rest + deps — 35/35", "spec_suite": "362 files / 9473 tests passed", "objectql_suite": "169 files / 2950 tests passed", "rest_suite": "77 files / 1228 tests passed", "runtime_suite": "119 files / 1870 tests passed", "metadata_protocol_suite": "67 files / 862 tests passed", "client_suite": "21 files / 279 tests passed", "root_gates": "check:route-envelope, check:doc-authoring, check:quick-reference-counts, check:nul-bytes — all green", "heavy_runs_flocked": "/tmp/os-heavy-verify.lock" }, "snapshots": { "dual_snapshot_rule": "satisfied in full, regenerated after a real build", "api_surface": "packages/spec/api-surface/api.json — PublishMetaItemResponse (type), PublishMetaItemResponseSchema (const)", "export_origins": "packages/spec/export-origins/api.json — both, resolved to src/api/protocol.zod.ts#...", "docs_reference_rows": "content/docs/references/api/protocol.mdx + index.mdx via the gen chain (1575 -> 1576 schemas)", "other_generated": "authorable-surface/api.json, json-schema.manifest/api.json, docs/audits/2026-07-unknown-key-strictness-ledger.counts.md (api/ 393 -> 397)" }, "deviations": [ "Added the spec-side pin suite in protocol.test.ts beyond the three artifacts named. Not scope creep: the conformance gate's mirrored header asserts 'the spec-side suite pins what the schema says; this one pins that the schema still matches what the code emits' — that sentence is false without it, and #5745 landed both halves.", "Also fixed packages/metadata-protocol/src/protocol.ts: publishMetaItem's Promise<...> annotation omitted projectionApplied while the implementation assigned it. Left alone, the new schema would declare a field the producer's own TS type denies. Same declared-!=-returned class, one layer down.", "The in-memory driver harness is duplicated into the new test rather than shared. save-meta-response-conformance.test.ts and plugin.authoring-channel.test.ts each already carry their own copy, so self-contained is the established shape; extracting would edit two landed files for no gate.", "Worktree setup: `git branch -D` was blocked by the permission classifier, so the shared checkout was detached and the pre-existing branch checked out in the worktree instead (same commit). Net effect identical." ], "docs_adr_touched": false, "special_inspection_notes": [ "MetadataProtocol (protocol.zod.ts:1840) still does NOT declare publishMetaItem — the REST layer reaches it via `(p as any).publishMetaItem`. Naming it on the interface needs a PublishMetaItemRequest type, i.e. a request schema, which the card scoped out. Left as-is; flagging for whoever wants the cast gone (Prime Directive #12).", "For #4717's metadata half: the asymmetry the card warned about is now GONE — both write doors have a declared, conformance-gated response envelope, so an advisory-findings field can be added to the two symmetrically.", "The `seed` conformance case had to be written env-wide (no organizationId): ADR-0005/#6190 refuse an org-scoped write for a type whose registry entry says allowOrgOverride:false, and `seed` is one. Documented at the fixture. The seed path exercises the real SUCCESS branch — {success:true, inserted:1, updated:0}, verified by probe, not the surfaced-failure fallback — so the assertion pins the counters rather than their types.", "No out-of-scope finding issues filed — nothing surfaced that was not either in scope or noted above.", "NO auto-merge enabled. PR #7356 is a DRAFT awaiting the PM's serial landing." ] }
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Aug 18, 2026 - added a commit that references this issue
on Aug 23, 2026 - added a commit that references this issue
on Aug 23, 2026
Observation-class finding, measured while scoping #4717 on
origin/main@ea8e8490f. Nothing a user hits today — the route works and returns a sensible body. Filing plainly and letting triage grade it.What was measured
publishMetaItemis served over REST:packages/rest/src/rest-server.ts:5468registersPOST {metaPath}/:type/:name/publish, and:5488awaitspublishMetaItem(...)and hands the result tores.json().packages/metadata-protocol/src/protocol.ts:9771-9798and carriessuccess/version/seq/message, plus the conditionalseedApplied/materializeApplied/projectionApplied.But the string
PublishMetaItemappears nowhere underpackages/spec/src/:And the endpoint itself is not in the REST declaration —
packages/spec/src/api/plugin-rest-api.zod.tsdeclares 6 endpoints withcategory: 'metadata', and none of them hashandler: 'publishMetaItem'or a/publishpath (grep -n publishon that file: zero hits).Why this is the same class as #5745, one door over
#5745 closed exactly this gap for the sibling write door, and its own prose states the discipline it was establishing.
SaveMetaItemResponseSchema(packages/spec/src/api/protocol.zod.ts:456) now declares the FULL save body, the endpoint declaresresponseSchema: 'SaveMetaItemResponseSchema'(plugin-rest-api.zod.ts:824), and a producer-side conformance gate keeps the two in step —packages/objectql/src/save-meta-response-conformance.test.ts, whose header reads:The publish door has none of those three: no declaration, no
responseSchemareference, no conformance gate. Soversionon the publish response is in the same undeclared stateversionon the save response was in before #5745 — and it is the same ADR-0008 OCC token, with the same "echo it back asIf-Match" job.Why it is observation-class rather than a defect
The route answers correctly and the body is well-formed; no caller is broken today. The cost is the one #5745 named: a public wire surface with no contract behind it, so a later field addition or removal is invisible instead of gated, and
PublishMetaItemResponsecannot be named at the type level the waySaveMetaItemResponsecan (packages/client/src/index.ts:18imports and re-exports the latter).Adjacent, and the reason this surfaced
#4717's metadata half puts the runtime authoring gate's advisory findings on the
saveMetaItemresponse. The gate runs on both doors —protocol.ts:9133for a direct active save, andprotocol.ts:9869for the draft-to-active promotion insidepublishMetaItem(deliberately, per #4463 D1, so?mode=draftthen/publishis not a bypass). So whoever designs the advisory field will face two response envelopes, one declared and conformance-gated, one not declared at all. That asymmetry is worth knowing before the shape is chosen, not after.Filed unassigned per Prime Directive #10. No
pm:queue— observation class.