Repository navigation
fix(plugin-audit): sys_activity.actor_name carries the acting user's display name, written with the row - #22526
objectstack-fleet[bot] wants to merge 3 commits into
Conversation
…display name, captured with the row The audit writer filled sys_activity.actor_id and never actor_name, a column the object declares and lists among its highlight fields, so every record History entry read "Unknown user". The name is now the acting user's sys_user title, read once per user (30 s memo) and only when an activity row is written. An unreadable name stays null, never the id; a write with no user stays null (ADR-0118 D1). Claude-Session: https://claude.ai/code/session_01WYYhVJ78u7PhwFViWo1EmQ Co-authored-by: Claude <noreply@anthropic.com>
…r's bound The double's find now applies the caller's limit after the filter, by presence, and reads are counted by a spy outside it, so check:objectql-double-limit grades it conforming instead of unjudged. Claude-Session: https://claude.ai/code/session_01WYYhVJ78u7PhwFViWo1EmQ Co-authored-by: Claude <noreply@anthropic.com>
…tivity-actor-name
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 9 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 50392259b6ba5ce4da257f114c9cd122b6a59ff3 && git checkout 50392259b6ba5ce4da257f114c9cd122b6a59ff3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ee8751d41e61a18f7819e4d3ad2c340f51ab2418 82440a8b41793ab58cf3c584466bc4ac25aa9263 && git checkout -B drift-repro ee8751d41e61a18f7819e4d3ad2c340f51ab2418 && git merge --no-ff 82440a8b41793ab58cf3c584466bc4ac25aa9263
node scripts/docs-audit/affected-docs.mjs --json ee8751d41e61a18f7819e4d3ad2c340f51ab2418 |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 37983264039 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Merge-queue red on PR #22526: a new signature, not re-queued by the seat; evidence says it is not this PR's
Signature: queue run 37983264039, Test Core (3/6), First reading (the three facts):
Seat's reading: most likely a flake in the Instrument note (not this PR's): the case cannot explain its own red, because it asserts |
Fixes #22510
Clause-②: no
What this changes
sys_activitydeclaresactor_nameand lists it among itshighlightFields, because its entries are "denormalized snapshots optimized for chronological … reads" (sys-activity.object.tsdocblock). The one writer of that table,plugin-audit'swriteAudit(audit-writers.ts), filledactor_idand neveractor_name. So every row named the user by id only, and the record History tab, which readsactor_name, showed "Unknown user" for every entry.The writer now records the acting user's display name when it writes the activity row:
sys_userrow, through the writer's existingresolveLookupTitles. That is the ADR-0079 title forsys_user(nameField: 'name', a required column). It is the same column the approval timeline reads its actor names from, and the same title this writer already renders for auserreference in a tracked-change summary.userIdthat lands inactor_id, so the two columns cannot disagree. This includes theattributedUserIdchannel, a better-auth write authorized as the system and credited to a human.sys_useror a failing read all leaveactor_nameunset. An id in a name column reads as a plausible name, andactor_idstill says who acted.enable.activitiesis notfalse) and a user is present. It is memoized per user for 30 s, and the promise itself is cached, so a predicate write over N rows costs onesys_userread, not N.writeAuditis dispatched once per row, and this is the hot path that plugin-audit captureBefore still fetches its own pre-image — retire the second read once the engine binds ctx.previous before every before* dispatch #6656 / PR fix(plugin-audit): consume the engine's boundctx.previousand record one normalised view on both sides of the diff (#6656) #6977 took reads off.sys-activity.object.tsis unchanged, and rows already written are not backfilled. The renderer half for those rows is record:history maps the entry's user from actor_name only, so activity rows written without it (every row before objectstack#22510's fix) read "Unknown user" although actor_id is set objectui#12067.Files:
packages/plugins/plugin-audit/src/audit-writers.ts,packages/plugins/plugin-audit/src/activity-actor-name.test.ts(new) and.changeset/22510-activity-actor-name.md(patch,@objectstack/plugin-audit).Reproduction on a real stack (before → after)
Stack:
pnpm dev:crm -- --fresh -p PORT, built from this worktree. Before =origin/main4e9fe9ff6; after = this branch atd2152c41d. The head82440a8b4mergesorigin/mainee8751d41and carries the sameaudit-writers.tsblob (fecc7a3d630c). A sales rep (Riley Rep, created by the admin throughPOST /api/v1/auth/admin/create-user, given thesales_repposition throughsys_user_position) creates acrm_leadand then edits it through/api/v1/data/crm_lead. The lead's History source rows are read throughGET /api/v1/data/sys_activity(filterobject_name+record_id).4e9fe9ff6)d2152c41d)createdrowactor_id= rep id,actor_name: nullactor_id= rep id (unchanged),actor_name: "Riley Rep"updatedrowactor_id= rep id,actor_name: nullactor_id= rep id (unchanged),actor_name: "Riley Rep"updatedrowactor_id= admin id,actor_name: "Dev Admin"sys_activityrow on the fresh stackactor_idnull and no name, 9 with a user and no nameactor_idnull and no name (unchanged), 10 with a user and a name (7 Dev Admin, 3 Riley Rep)Pins (
src/activity-actor-name.test.ts, through the realObjectQLengine and a copy-returning, read-counting driver)actor_idis the same id (control)attributedUserIdnames that human;actor_idis the same idsys_userrow, and a user with a blank name, leaveactor_nameempty, never the idisSystem), and one by a service principal onactor, leaveactor_nameempty and read no user (ADR-0118 D1 / D5)sys_userread; a later write by the same user costs 0enable.activities: falsewrites no activity row and reads no userPin 1 reads the STORED
sys_activityrow (the driver's store), not a renderer. That is the server-side proof for every reader of the column.Ablation (each leg through
scripts/ablation-replace.mjsWRAP mode, the fix committed first, restore proven to the HEAD blobfecc7a3d630c,git diff HEADempty; last run on head82440a8b4)audit-writers.tsactor_namestamp from the row literalexpected undefined to be 'Ada Lovelace'), 3 / 4 / 6 green, 3 failed / 3 passed?? null→?? userId)expected 'usr_ghost' to be null), 1 failed / 5 passed'System'expected 'System' to be null), 1 failed / 5 passedexpected 3 to be 1), 1 failed / 5 passedenable.activitiesgate is ignored for the readexpected 1 to be +0), 1 failed / 5 passedTwo first attempts did nothing and are reported, not hidden. Leg C's anchor also matched the locale memo (
ANCHOR AMBIGUOUS, nothing written). Leg D's replacement was a substring of its own anchor, so the tool refused its evidence and restored. Both were re-run with unique anchors as C2 and D2 above. The test imports./audit-writers.jsfromsrc, so nodistrebuild was involved in any leg.Open decision: the system-context label (not implemented here, so the line above is
Part of)Triage asked that "a system-context write records the platform's system label, not null". That conflicts with an accepted ADR, which binds until a superseding ADR says otherwise (AGENTS.md Prime Directive #13). ADR-0118 D1, verbatim:
D5 keeps the actor two-valued: a user, or the system as
null. No named constant for a system label exists in this repository. The only label is objectui'sdetail.systemActori18n key ('System','系统', …). objectui's activity feed (recordActivityFeed.ts,row.actor_name ?? systemActorLabel) already renders it per viewer for a row with no name. So this PR leaves system rows exactly as they were and pins that (pin 4; leg B2 shows the pin catches a written label). The question is handed to the seat. If the maintainer rules for a written label, the change is that pin, the one line it guards, and an ADR-0118 amendment.Gates
All measured on head
82440a8b4(this branch merged withorigin/mainee8751d41), after a full workspace rebuild.pnpm --filter @objectstack/plugin-audit test: 43 files, 678 tests passed (exit 0, throughos-verify-lock).pnpm --filter @objectstack/plugin-audit typecheck: exit 0. The new test file is intsconfig.test.json's program (--listFilescount 1), andcheck:test-typecheckis OK.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 67 commands derived from the 3 changed paths. All 67 were run with their exit codes recorded, and all 67 exited 0.--ranreconcile: "67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED".d2152c41d),check:objectql-double-limitwent red: the new test's store double ignored the caller'slimitand could not be probed. Fixed in0cff02f38: the double applies the bound after the filter, and reads are counted by a spy outside it. The gate then graded it conforming (278 conforming doubles, up from 277; none new in the baseline).eslint --no-inline-config --format jsonover the 2 touched TypeScript files gave 2 files, 0 errors, 0 warnings. Both are in the config's population (--print-configexit 0). This repo'seslint.config.mjsnever enables type-aware linting, so this diff cannot move the verdict on an untouched file. The repo-widepnpm lintis CI's.dispatch-gateslists outside its runnable set.Acceptance notes
The system-actor question is ruled (seat,
6087899596): option A, under accepted ADR-0118 D1. A system write keepsactor_namenull, and the label is the renderer's. The open decision is closed, so line 1 isFixes #22510.One write covers all three renderers.
record:history,record:activityand the record Discussion feed all read the samesys_activity.actor_namecolumn. Therepo:hotcrmseat measured the second and third on 17.7.0: every entry was authored "System", because objectui's feed readsactor_name ?? systemActorLabel. The same null showed as "Unknown user" in History. This PR fills the column at write time, so all three get the name from the row. The proof is the server-side pin on the stored row (pin 1) and the after-stack rows above. ⛔ No objectui edit: the renderers, and rows written before this change, are record:history maps the entry's user from actor_name only, so activity rows written without it (every row before objectstack#22510's fix) read "Unknown user" although actor_id is set objectui#12067's lane. One case still reaches the renderers with no name: a user row whose name cannot be read (pin 3). The feed shows that row as "System" until card objectui#12067'sactor_idlookup lands.Premise correction (no effect on the direction). Triage cites
approval-service.ts:7103as "another writer already fills it". Measured, that line is inlistActions. It enrichessys_approval_actionrows withactor_namewhen they are read, for the approval timeline. It is not asys_activitywriter and does not run at write time. No writer anywhere filledsys_activity.actor_name: zeroactor_namewrites ingit grepoverpackages/**at4e9fe9ff6, and 0 of 9 user rows named on the before stack. The "fill it" direction still stands on the object's own docblock.Email arm not copied. The approval reader falls back from
nametoemail. This writer readsnameonly.sys_user.nameisrequired: true, and a denormalized row every record reader can see is not where an address should be copied.Staleness is bounded and deliberate. A rename shows on rows written after the 30 s window. Rows already written keep the name they were written with, which is what a snapshot is.
Observed, not filed (a deliberate ruling). On
examples/app-crm, asales_repgets403 PERMISSION_DENIEDonGET /api/v1/data/sys_activity, even for its own lead's rows, so its History tab is empty there.default-permission-sets.tsexcludessys_activityon purpose ("a separate question if it ever matters"). hotcrm grants it.Found, for the seat to route (same family, not fixed here).
sys_activity.actor_avatar_urlis declared and never written either. On the after stack, the admin'ssys_user.imagewas set throughPOST /api/v1/auth/update-user, and the admin's next edit wroteactor_avatar_url: null. All 39 rows carry null. objectui reads it (record-history.tsxuser_avatar,recordActivityFeed.tsactorAvatarUrl).record-history.tsxrenders a system row (noactor_id, no name) as "Unknown user". ADR-0118 D1 says such a row renders the localized "System" label, which its siblingrecordActivityFeed.tsalready does. Theactor_idfallback in record:history maps the entry's user from actor_name only, so activity rows written without it (every row before objectstack#22510's fix) read "Unknown user" although actor_id is set objectui#12067 does not reach a row with noactor_id.Generated by Claude Code