Skip to content

feat(runtime,metadata-protocol,client,console): Studio publish takes a change note; item history names the person - #28422

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-64y985
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-64y985

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

domain:ui#5 · os-sales · session_01L6bhgUAnBEBqJCebezxh8P

Requested by Sales · project thread

Part of #28406 — items 1, 2, 3 and 5. Item 4 (the publish-drafts request schema in packages/spec, route ledger, OpenAPI) stays on the card for the spec lane, per the split note on the card.

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:Studio 发布时可以写一句变更说明,它会写进这次发布的每一条元数据历史;历史面板显示操作者的名字(查不到时显示 id 并标注"未解析的用户"),说明显示在该行下面。
  • 为什么改:历史里只有一串用户 id 和一句固定的 publish app package 'PACKAGE_ID',管理员挑版本回滚时看不出是谁、为什么改的;另外发布门把请求体里的 actor 当作操作者,持有 manage_metadata 的人可以冒名。
  • 风险与回滚:发布门现在只认会话身份,请求体 actor 被忽略;不写说明时行为与之前一致。回滚即还原本 PR,没有数据迁移。
  • 先看:packages/runtime/src/domains/packages.ts(publish-drafts 门取会话身份、转发 message)、packages/metadata-protocol/src/protocol.ts(每项 promote 的 change_note)、ui/packages/app-shell/src/views/item-tools/historyActors.ts(姓名解析)。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-publish-drafts-change-note.integration.test.ts,预期 4 个用例全绿(真实 SQLite 里每条历史行带说明、记录会话用户)。
  • 需要你做的:无,席位落地。

What changes

Runtime door (POST /packages/:id/publish-drafts only). The door forwards a string message from the body, and records the actor as the user id of the request's authorized execution context (resolvePackageWriteActor, the rule RestServer.resolveMetaWriteActor holds for the /meta write doors). A body actor is ignored; a context with no user id records no actor. The six sibling package doors are untouched (carried by #28414).

Protocol. publishPackageDrafts passes request.message to each promoteDraftForPublish in place of the hard-coded publish app package 'PACKAGE_ID', so every sys_metadata_history row the batch writes stores it as change_note. The package commit already kept it. No note, or an empty one, keeps today's sentence.

SDK. packages.publishDrafts(id, opts) gains opts.message. opts.actor stays in the type, marked @deprecated (the door ignores it), so no caller breaks.

Console.

  • MetadataClient.publishPackageDrafts picks message beside backfill and sends it only when given (no options still sends {}).
  • The publish review (DraftChangesPanel footer) offers an optional change note where it publishes one package as a batch (onPublish && packageId, the same condition as the backfill choice). Sent trimmed; an empty note sends nothing, and a sheet with neither a note nor a backfill choice still calls its host with no options. StudioDesignSurface.tsx is not edited: its doPublish(options) already hands the sheet's options to publishPackageDrafts(packageId, options).
  • HistoryPanel shows each actor by name, read from sys_user once per set of ids (historyActors.ts). An id with no row (a deleted user), a failed or refused directory read, or no adapter shows the id marked "unresolved user". A null actor shows none. The event's message is shown under the row; HistoryEvent gains message.
  • Strings: engine.edit.historyBy / historyActorUnresolved / historyActorUnresolvedHint in the studio namespace (en, zh); preview.changes.noteLabel / notePlaceholder in all ten locale packs.

Branch: the harness-assigned claude/project-thread-64y985, not claude/issue-28406-*; the card is #28406.

Tests

  • packages/runtime/src/domains/packages-publish-drafts-change-note.integration.test.ts (new, real ObjectQL + SqlDriver over better-sqlite3, through HttpDispatcher.handlePackages): a note reaches every history row and the commit, and historyMetaItem reads it back as message with the session user as actor; no note keeps the sentence; a body actor is ignored and the session user is recorded on every row and the commit; a system context records no actor.
  • ui/packages/app-shell/src/preview/__tests__/DraftChangesPanel.changeNote.test.tsx (new): the field is offered only on the batch door with a Publish, disabled while publishing; a note is sent trimmed as { message }; empty or whitespace sends no options; the note rides beside a checked backfill choice.
  • ui/packages/app-shell/src/views/item-tools/ResourceHistoryPage.actorAndNote.test.tsx (new): name shown with the id on its title and one directory read for the distinct ids; a missing row and a failed read show the id marked unresolved; a null actor shows none; notes under rows.
  • ui/packages/data-objectstack/src/metadata-client.publishMessage.test.ts (new): the body bytes with and without a note.
  • Moved pin: packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts asserted the exact protocol request { packageId, context }; it now carries actor: the caller's userId, the change this PR makes.

Acceptance notes

  • message follows the single-item publish door's rule: a string is forwarded, any other type sends no note (no 400). Item 4's request schema is where a stricter shape would be declared.
  • The unresolved marker does not say "deleted": a missing row and a refused directory read look the same from the console, and naming the wrong one would be a made-up fact on an audit surface. The hint text names both causes.
  • The claim's Metadata-protocol: yes is outside the closed vocabulary the changeset gate reads (added | narrowed | semantic | none). Nothing a metadata author writes changes here (a REST body key, a history note, the console), so this PR declares none, and the changeset is patch.
  • The claim named ui/packages/i18n/src/locales/{en,zh}.ts; the publish-review keys live in the locale packs, which all-locales-key-parity.test.ts holds to every en key, so all ten packs carry them. The history panel's keys live in the studio namespace, which is en + zh by design.
  • usePublishAllDrafts.ts (Home banner, draft-preview bar) is unchanged: neither surface has a review step with a note, and a parameter no caller passes would be declared but never written.

Verification

Local readings. Tests and ablations ran on 90af062b (the fix and its tests); the merge of origin/main (dc581177) brought in no file of the six packages touched here. Gates ran on dc581177; the changeset gates were re-run on db993774 after a wording-only changeset edit. The head, 5d035cf3, adds one string-only commit on top (full-width punctuation in the zh engine.edit.historyActorUnresolvedHint); @object-ui/i18n tsc --noEmit exit 0 and check-nul-bytes OK on it.

Tests (all green)
  • @objectstack/runtime: the 52 package-door and dispatcher files (src/domains/packages*.test.ts, src/package-*.integration.test.ts, src/http-dispatcher.test.ts, src/dispatcher-validation-error*.test.ts): Test Files 52 passed, Tests 1106 passed, after the one moved pin above (it was the one red: expected … to deeply equal, the new actor key).
  • @objectstack/objectql publish and commit suites (7 files): 94 passed.
  • @objectstack/metadata-protocol whole package: Test Files 285 passed / 4 skipped, Tests 21872 passed / 26 skipped.
  • Console, from ui/ root: the DraftChangesPanel suites, views/item-tools, the two existing HistoryPanel suites, usePublishAllDrafts suites and the metadata-client publish suites: Test Files 41 passed, Tests 257 passed.
  • Typecheck, each exit 0: @objectstack/metadata-protocol, @objectstack/client, @objectstack/runtime (with its test layer), @object-ui/i18n, @object-ui/data-objectstack, @object-ui/app-shell (tsc --noEmit and tsconfig.test.json).
Ablations (each leg mutated through `scripts/ablation-replace.mjs`, restored blob == HEAD)
  • Protocol: the per-item note forced to the default (? request.message replaced by a globalThis marker), metadata-protocol rebuilt, marker proved in dist/ by ablation-dist-preflight → the integration file 1 failed / 3 passed (the note test). Restored, rebuilt, marker proved absent → 4/4. A first attempt with the anchor message: request.message was refused as ambiguous (4 hits); nothing was written.
  • Door: the actor read back from body?.actor → 3 failed / 1 passed (note-with-actor, body actor ignored, system context).
  • Publish review: the message key never sent → 2 failed / 5 passed.
  • History panel: names never passed to the row → 3 failed / 1 passed; the note never drawn → 1 failed / 3 passed.
Derived gates (`node scripts/pm/dispatch-gates.mjs --commands`, 67 families)
  • 65 exit 0 on dc581177.
  • pnpm --filter @objectstack/spec run check:skill-examples and pnpm check:dual-build-cjs-loads, each exit 0 on db993774. Their first run exited 3 (prerequisite, not a verdict: no generated schema and 34 packages without dist/ on a fresh clone); re-run after gen:schema and a full turbo run build (117 tasks, exit 0). --ran reconcile: 67/67, 0 NOT-MEASURED.
  • Re-run on db993774: check-changeset-no-major, check-empty-changeset, check-adr-0087-registration (all --base origin/main) and check:doc-authoring, each exit 0.
  • Dogfood battery: not owed (no ! in the title, no (narrowing) arm).

Generated by Claude Code

…nge note; item history names the person

The package publish-drafts door forwards the body's `message` and records
the request's authenticated user as the actor (a body `actor` is ignored).
The protocol threads the note into every item promote, so each history row
the publish writes carries it as `change_note`; the package commit keeps it.

Studio's publish review offers an optional change note on the batch door,
and the item history panel shows the actor's name (the id, marked
unresolved, when no user row answers) and the change note under the row.

Claude-Session: https://claude.ai/code/session_01L6bhgUAnBEBqJCebezxh8P
Co-authored-by: Claude <noreply@anthropic.com>
…panel's names and notes

Claude-Session: https://claude.ai/code/session_01L6bhgUAnBEBqJCebezxh8P
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/client, @objectstack/metadata-protocol, @objectstack/runtime, touching 3 documentable anchor(s).

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

  • content/docs/permissions/system-context.mdx (via handlePackagesRequest (symbol, a top-level function))

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

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

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
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 212 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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 — 43 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 380a1078f153e628eeef2285a75bcf060df35905 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3dd1566002a43bdd29d794a5593b744c2b2d27b9 — the merge of head 5d035cf37ef8b022b0acbf016f107328495eebd7 into base 380a1078f153e628eeef2285a75bcf060df35905, 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 3dd1566002a43bdd29d794a5593b744c2b2d27b9 && git checkout 3dd1566002a43bdd29d794a5593b744c2b2d27b9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 380a1078f153e628eeef2285a75bcf060df35905 5d035cf37ef8b022b0acbf016f107328495eebd7 && git checkout -B drift-repro 380a1078f153e628eeef2285a75bcf060df35905 && git merge --no-ff 5d035cf37ef8b022b0acbf016f107328495eebd7

node scripts/docs-audit/affected-docs.mjs --json 380a1078f153e628eeef2285a75bcf060df35905

⚠️ 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 380a1078f153e628eeef2285a75bcf060df35905 → 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:ui#5 · os-sales · session_01L6bhgUhKx1XHzkoikGjRB4

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 5d035cf37ef8b022b0acbf016f107328495eebd7
Local-runs: none
Scope: packages/runtime/src/domains/packages.ts, packages/client/src/index.ts, ui/packages/data-objectstack/src/metadata-client.ts + packages/metadata-protocol/src/protocol.ts, packages/rest/src/rest-server.ts

① Derived judgments

The face is the Clause-②: yes (widening) declaration alone: no file under packages/spec/src/** and no governed path is in the diff (check-governed-merges --pr 28422: 0 of 25 paths governed). What widens: POST /packages/:id/publish-drafts now reads a string message from its body (any other type sends no note, no 400), and @objectstack/client packages.publishDrafts opts gain message?: string; MetadataClient.publishPackageDrafts options pick 'backfill' | 'message' off that type. The protocol request type already declared message (protocol.ts ≈L23787), so the method's accept set is unchanged; only the per-item note now reads it, with the old sentence as the fallback when it is absent or empty.

② Semver level

patch for all six packages, as the changeset declares: a new optional key on a request body and an optional opts member, no removal. opts.actor stays in the SDK type (@deprecated), so no caller fails to compile. Behaviour change on the door: a body actor is now ignored and the authorized context's userId is recorded (no user id ⇒ no actor), which is the tightening item 2 of #28406 asks for, not a widening.

③ Boundary flags

resolvePackageWriteActor reads context.executionContext.userId, the same context requireManageMetadata judged on this request, and returns it only as a non-empty string: the rule RestServer.resolveMetaWriteActor (rest-server.ts ≈L3070) holds for the /meta write doors. Only publish-drafts reads it; the six sibling doors still read body.actor and are carried by #28414, as the card's "Not here" says. No other door, schema or published type moves. Item 4 (a request schema in packages/spec) is left on the card for the spec lane (split note 6543345540).

Implemented-by: session_01L6bhgUAnBEBqJCebezxh8P
Reviewed-by: session_01L6bhgUhKx1XHzkoikGjRB4

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · os-sales · session_01L6bhgUhKx1XHzkoikGjRB4

ACCEPT · 2026-10-31T14:06Z
Head-sha: 5d035cf

席位核对

  1. Shape: 25 files (+819 / −23): the publish-drafts door in packages/runtime/src/domains/packages.ts (forwards a string message, records the authorized userId, ignores a body actor), the per-item note in publishPackageDrafts (packages/metadata-protocol/src/protocol.ts), the SDK opts in packages/client/src/index.ts, MetadataClient.publishPackageDrafts, the note field in DraftChangesPanel.tsx, actor names and notes in HistoryPanel (ResourceHistoryPage.tsx, new historyActors.ts), i18n, four new test files, one moved pin, one patch changeset. Inside claim 6543324409; packages/spec/src/** and StudioDesignSurface.tsx untouched. Part of #28406, no closing keyword. Not governed, 842 lines (check-governed-merges --pr 28422).
  2. Premise, measured at claim: the door passed body.actor and backfill, never message; protocol.ts hard-coded publish app package '<id>' on every history row; the panel printed the raw id. The diff fixes each at its site.
  3. Contract: Clause-②: yes (widening), declaration face only; TIER_DEFAULT review record by this seat, PASS on this head (6544420721).
  4. Gates (check-runs on this head): 67 runs, 57 success, 10 skipped by design (opt-in / scheduled / docs), 0 other; Test Core, Test UI, Lint & Repo Gates, TypeScript Type Check, Check Changeset, Governed Surface Queue Guard success. The dev's local battery: dispatch-gates --commands 67/67, 0 NOT MEASURED after the prerequisites were built; five ablations each red then restored.
  5. Deviations accepted: Metadata-protocol: none in place of the claim's yes (the changeset gate's vocabulary is added | narrowed | semantic | none, and no metadata author's input changes); the publish-review keys land in all ten locale packs (all-locales-key-parity.test.ts); usePublishAllDrafts.ts unchanged (neither of its surfaces has a review step). At the seat's request the zh hint was set in full-width punctuation (5d035cf3, one line) and the PR body's Verification now carries the re-run readings.
  6. Out of scope: the six sibling package doors' actor (runtime: the package doors record whatever actor the request body names — publish, save, discard, revert, rollback, reassign and duplicate write a caller-chosen name into metadata history #28414); item 4, the request schema in packages/spec (split note 6543345540).
  7. Changeset, read sentence by sentence: patch for six packages; each paragraph (note reaches every history row, empty keeps the old sentence, actor is the signed-in caller with NULL for a system context, opts.actor kept deprecated, Studio note field and history names) is true against the diff.
  8. Landing: pr_ready + automerge_enable through the relay, MERGED confirmed, then the partial-landing release of Studio: publishing takes a change note and item history names the person — the note reaches publish-drafts, the actor is the session user, and it is shown by name, not id #28406 to pm:queue with item 4 named as the remainder.

Generated by Claude Code

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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant