Repository navigation
fix(metadata-protocol): a metadata row records who created it and who last changed it - #23067
Merged
objectstack-fleet[bot] merged 4 commits intoOct 12, 2026
Merged
Conversation
…d before the fix) Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…nd who last changed it SysMetadataRepository.put stamps created_by on insert and updated_by on every write from PutOptions.actor, as its header's actor -> updated_by mapping declares. A missing actor stores NULL in both, never a placeholder user. History's recorded_by is unchanged. Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…ent-wide repository listDrafts reads Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 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 — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Oct 12, 2026
objectstack-fleet
Bot
deleted the
claude/issue-23033-metadata-row-actor
branch
October 12, 2026 16:25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #23033
Clause-②: no
SysMetadataRepository.putnow writes the actor onto thesys_metadatarow. It stampscreated_bywhen the row is first written andupdated_byon every write, both fromPutOptions.actor, as the repository's own header mapping (actor → updated_by) declares. A write with no actor stores NULL in both columns, never a placeholder user. History'srecorded_byis unchanged. This follows the triage direction (comment 6124803255).Measured first, on
origin/maindf7a137923saveMetaItemwithactor: 'usr_alice') on a real kernel (ObjectQLPlugin: the real engine, the real audit hooks, the realsys_metadatadeclaration) leftcreated_byandupdated_byabsent on the stored row (expected undefined to be 'usr_alice', 3 of the 4 new real-engine pins red). The history row'srecorded_bycarried the actor (the existingsys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556 pins stayed green).put()builtparentRowDatawith neither column and wrote it withcontext: { ...ctx, isSystem: true }, a context that carries no user. The engine's audit hooks (sys_stamp_audit_insertandsys_stamp_audit_updateinobjectql/src/plugin.ts) stamp the*_bypair only insideif (session?.userId), so they wrote nothing either.isSystemwrite? No. With nosession.userIdthe hooks assign nothing, andisSystemexempts the write from the static-readonly strip on insert and on update (created_byisreadonly: trueonsys_metadata). So the pins read the row the driver stored, not the payload. The real-engine file exists to pin exactly this.actor. The REST/metadoors pass the authenticated user id orundefined(resolveMetaWriteActor, A bearer-authenticated admin metadata write is stampedactor: 'system'—req.user/req.userIdare unset on the/metaPUT path #7749 and [Decision] ShouldX-Actorkeep outranking the authenticated identity on/metawrites? (surfaced by #7749) #7941;'system'is only the audit-row default,recordMetadataAudit).saveMetaItemforwardsrequest.actor ?? null,promoteDraftandrestoreVersionforward their ownactor, and boot or internal writes pass none. The columns arelookup('sys_user'). UnderisSystemthe engine runs no reference check, so a non-user string would be stored as given. The repository stamps exactly whatrecorded_byalready records: one value, two columns. See the acceptance notes for the one producer that passes a label.Other writers of the parent row in this class
put: the insert and the update, both stamped (the only twoengine.insert/engine.updatecalls onsys_metadatain the class).promoteDraftandrestoreVersion: both write the active row throughput(..., { actor: opts.actor }), so they get the same stamp. A publish or a restore that creates the active row records its own actor ascreated_by; one that updates the row movesupdated_by.deleteanddropPromotedDraftRow: both hard-delete the row, so no row is left to stamp. The tombstone'srecorded_bykeeps the actor, as before.rowToItem'sauthoredByandlistDrafts'updatedByreadupdated_by ?? created_by ?? null, and with the write side stamping, both name the last writer (pinned below).Change
packages/metadata-protocol/src/sys-metadata-repository.ts:updated_by: opts.actor ?? nullin the rowput()builds, andcreated_by = opts.actor ?? nullon the insert branch only, beside the existingupdated_at/created_at. The header's mapping table gains thecreated_byrow..changeset/23033-metadata-row-actor.md:patchfor@objectstack/metadata-protocol.Pins
packages/metadata-protocol/src/sys-metadata-repository.recorded-by.test.ts: a new#23033block beside thesys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556 pins, on the file's existing engine double (no new double, no ledger change). It covers first write A, then A/A; second write B, then A/B with history[A, B]; no actor, then NULL/NULL; an authored write followed by an actor-less one, then A/NULL; and the read side (get().authoredByandlistDrafts().updatedByname the last writer).packages/objectql/src/protocol-metadata-row-actor.test.ts(new): the same rule through the door, read off the row the real engine stored. It covers first write A, then A/A with history[A]; second write B, then A/B with history[A, B]; the CONTROL (no actor, then both null, history[null]); andlistDraftsansweringupdatedByfrom the draft row. It sits inobjectqlbecausemetadata-protocolmust not depend onobjectql, the same reasonplugin.authoring-channel.test.tssits there.trap-guarded; restore bygit checkout HEAD --, proved by blob == HEAD and an emptygit diff HEAD). Both stamp lines were deleted throughscripts/ablation-replace.mjs(anchor 1 to 0 each),metadata-protocolwas rebuilt, andablation-dist-preflight --absentconfirmed both markers were gone fromdist/.sys_metadata_history.recorded_by是lookup('sys_user')却存哨兵字符串'system'——声明的类型与实际存的值不是一回事 #4556 pins stayed green.dist/index.js, whole tree clean, 13/13 and 4/4 green.Verification (head
d4ac870f46, after mergingorigin/main0cbfb7c32c)@objectstack/metadata-protocolvitest run: 227 files passed, 3 skipped; 28197 tests passed.typecheck(tsc --noEmit,include: src/**/*, tests included): exit 0.@objectstack/objectqlvitest run --project local: 407 files passed, 7940 tests passed.typecheck(tsc --noEmit && check:test-typecheck): exit 0, and the new test file compiles undertsconfig.test.jsonwith no debt entry.pnpm check:dual-build-cjs-loads, which is NOT MEASURED: it printedPREREQUISITE NOT METbecause the whole workspace'sdist/is not built in this worktree, and CI runs it.integration-tier and other whole-tree runs are left to CI.Acceptance notes
actor(out of scope here; reported for the seat to file).migrateStoredMetadatadefaultsactorto'migrate-stored', and its three callers pass labels, not user ids:POST /meta/_migrate-storedpasses'USER (POST PREFIX/_migrate-stored)'(rest-server.ts), the runtime door passes the same shape (runtime/src/domains/meta.ts), and the CLI passes'os migrate meta --stored'.PutOptions.actordeclares the value lands in alookup('sys_user')and must benull, never a label, when there is no user id. Those labels already reachrecorded_by. With this change they also reachupdated_byon the rows the pass rewrites. The repository cannot tell a label from a user id, and contract-first puts the fix at the producer, which also lies outside this card's file surface. The fix is to pass the user id ornulland carry the door's name insource.sys_metadatalives outside this class.persistDatasourceRow(service-datasource/src/datasource-admin-plugin.ts) inserts and updates the row directly, with no actor in hand and no history row. A/meta-written datasource row that is later edited through the datasource admin door keeps the/metawriter'supdated_by(read from the code, not run). Not touched; there is no carrier.InstallPackageRequestSchema) is a new contract key and is deliberately not here, per triage.Generated by Claude Code