Repository navigation
feat(spec,plugin-auth): a demoStaff entry names its line manager, and os dev links it - #26763
Conversation
…provisioning second pass Claude-Session: https://claude.ai/code/session_01WGufzehKYBxrrAG5Jid68s Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzehKYBxrrAG5Jid68s Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzehKYBxrrAG5Jid68s Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzehKYBxrrAG5Jid68s Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzehKYBxrrAG5Jid68s Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzehKYBxrrAG5Jid68s Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 152 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 9c21e4dc9054fe1a95c93336c58a51f24d1b8efc && git checkout 9c21e4dc9054fe1a95c93336c58a51f24d1b8efc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a485b5a3e28d84550e8a33bbb5cfc2d52c82ff9e 53d29e263c2573b2442cd17af01eedab1e2c8d4b && git checkout -B drift-repro a485b5a3e28d84550e8a33bbb5cfc2d52c82ff9e && git merge --no-ff 53d29e263c2573b2442cd17af01eedab1e2c8d4b
node scripts/docs-audit/affected-docs.mjs --json a485b5a3e28d84550e8a33bbb5cfc2d52c82ff9e
|
Contract reviewServed-tier: Inputs read: card #26616 (body, the re-lane note, the triage grade, the claim, the os-dev-report); PR #26763 body and file list; the net diff Gate verdicts on the head (check-run conclusions, read 2026-10-27 at the time of this record): green — ① Derived judgments
② Semver levelChangeset ③ Boundary flags
④ Design judgment
Cited code, read to judge the above (not reviewed; for the lane seat's ACCEPT at
Must-fixes: none. Should-fixes: three (the schema TSDoc clause; the depth-cap sentence in the acceptance note; the dropped logger on Implemented-by: VERDICT: PASS |
…the boot logger to the manager links Claude-Session: https://claude.ai/code/session_01WGufzehKYBxrrAG5Jid68s Co-authored-by: Claude <noreply@anthropic.com>
席位核对 · ACCEPT ·
|
Requested by Yibo Wang · project thread
Fixes #26616
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: #26764
维护者速读
demoStaff条目新增可选键manager(填同一列表里另一条目的 email);os dev启动时在所有演示账号建好之后、种子数据之前写入sys_user.manager_id。manager一级找不到人,只能手工去 Setup → Users → Set Manager 补。os dev启动且声明了manager的应用;已有上级(含手工改过的)一律不覆盖。回滚即 revert 本 PR,未声明manager的栈行为不变。packages/plugins/plugin-auth/src/demo-staff.ts的linkDemoStaffManagers;packages/spec/src/stack.zod.ts的collectDemoStaffManagerErrors;packages/spec/src/system/demo-staff.zod.ts的manager。pnpm --filter @objectstack/plugin-auth exec vitest run src/demo-staff-provisioning.test.ts,应见 11 个用例全绿(含 8、9 两个上级用例)。What changes
Spec (
packages/spec/src/system/demo-staff.zod.ts). AdemoStaffentry gains one optional key,manager: theemailof another entry of the samedemoStafflist, compared without regard to case. A value that is not an address (the likely mistake: writing the manager's display name) is refused at the entry's path with a message saying it takes another entry's email.reportsTo,managerEmail,managerId(which also catchesmanager_id, one probe) andsupervisorare aliases that point tomanager.Why the email and not the name. The address is the identity key on every path that provisions an account: the provisioning finds each entry's account by it (
findUserIdByEmail), it is what makes a re-run idempotent, and the admin bulk import resolves itsmanager_idcolumn by an identity key too, never a display name. A display name is a Tier-1 profile field the person can edit on their own profile, and it is the seed's reference key, not the account's. The schema already checks the address's shape, so a typo in it is caught at the entry before the stack check runs.Stack check (
packages/spec/src/stack.zod.ts,collectDemoStaffManagerErrors, called fromcollectDemoStaffErrors).defineStack(and soos validateand every load) refuses, withSTACK_CROSS_REFERENCE_INVALID422, each of:managerthat is the address of no entry;stack.demoStaff[i]origin; an entry that only hangs from a loop is not named in it.None of these is skipped: each would leave a manager link that the boot cannot write.
Provisioning (
packages/plugins/plugin-auth/src/demo-staff.ts, the first reader).provisionDemoStaffkeeps its first pass unchanged and adds a second one,linkDemoStaffManagers, after every entry has had its turn and still on the samekernel:ready, so before the application seed. For each entry with amanager:failedunder the subordinate's address, naming whether the manager's entry was not provisioned or is not an entry at all;manager_idalready equals the declared manager: nothing written;managersKept(aninfoline on the boot log names them);applyUserManagerLink, the one derivation behindPOST /api/v1/auth/admin/set-user-manager. Its refusals (self, cycle, depth, cross-organization, directory-owned) apply unchanged and are reported with theircodeandreason; this module carries no copy of them.DemoStaffOutcomegainsmanagersLinkedandmanagersKept.auth-plugin.tsadds both to the boot's summary lines.A re-run never overwrites a manager set by hand. The declaration wins only over absence. A manager that an operator cleared by hand reads as no manager, so the next boot writes the declared one again; the alternative (remembering which links the boot wrote) needs state outside
sys_userthat nothing else reads, for the one case where an operator wants a demo account to have no manager at all and also keptmanagerin the stack.Reference docs.
content/docs/references/system/demo-staff.mdxregenerated (gen:docs),authorable-surface/system.jsongainssystem/DemoStaff:manager.Why
A fresh demo's approval ladder starts at the requester's manager, and
manager_idwas NULL on every demo account (measured in hotclm on 18.2.0: 11 of 11), so rung 1 reached nobody until someone set it by hand. Only the platform can writemanager_id, so the application could not declare it.Tests
Runs through
scripts/pm/os-verify-lock.sh. Source as of8e1dde389; heada516ef709adds only the engine-double ledger line.Spec: src/stack-demo-staff.test.ts (9 new cases) and the full suite
describe('manager — …'): accepts a manager declared after the entry, case-insensitively; accepts a three-link chain that reaches a top; refuses a display name atdemoStaff.0.managerwith the message; refusesreportsTo/managerEmailnamingmanager; refuses a manager naming no entry (STACK_CROSS_REFERENCE_INVALID422 + first sentence); refuses self-reference (case-insensitive); refuses a two-entry loop once, naming both; refuses a three-entry loop that a fourth entry hangs from, naming only the loop.pnpm --filter @objectstack/spec teston8e1dde389's parent tree:Test Files 1 failed | 730 passed (731),Tests 1 failed | 21928 passed | 1 todo. The one failure wassrc/shared/alias-integrity.test.ts: mymanagerIdandmanager_idaliases share the probemanagerid. Fixed by droppingmanager_id(commit8e1dde389); re-run ofalias-integrity.test.ts+stack-demo-staff.test.ts:Test Files 2 passed,Tests 42 passed.pnpm --filter @objectstack/spec typecheck: exit 0.plugin-auth: unit and real-boot pins, and the full suite
src/demo-staff.test.ts, newdescribe('provisioning — each declared manager is linked …'), 6 cases on an engine double that records every update: fresh (the manager declared after the entry is linked, onemanager_idupdate); re-run with the link in place (no update); re-run with a manager set by hand (no update,managersKept: ['counsel1@legal.example']); re-run after a manager was cleared (written again); the manager's entry not provisioned (reported on the subordinate, exact message, no update); the set-manager write refusing ([PERMISSION_DENIED, idp_provisioned], no update).src/demo-staff-provisioning.test.ts(realObjectKernel, ObjectQL on in-memory SQLite, realAuthPlugin+SecurityPlugin), new cases 8 and 9: a fresh boot leavesmanager_idset for both orders of declaration (managersLinked: 2); the next boot keeps a manager re-pointed by hand and re-writes a cleared one (managersLinked: 1,managersKept: ['para1@legal.example'], nowarn).Test Files 1 passed,Tests 11 passed.pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2:Test Files 198 passed (198),Tests 3788 passed | 10 skipped.pnpm --filter @objectstack/plugin-auth typecheck(after building plugin-auth's own dist, whichexamples/basic-usage.tsimports): exit 0.Ablation (one, on the behaviour the card asks to keep)
node scripts/ablation-replace.mjs --file packages/plugins/plugin-auth/src/demo-staff.ts --anchor " if (current !== null) {" --replacement " if (current !== null && false) {" -- … vitest run src/demo-staff.test.ts: anchor x1 → x0, blob5ed3b373be04→7c15b3750852;× a re-run leaves a manager set by hand as it is, and says so,Tests 1 failed | 19 passed; restored, blob == HEAD andgit diff HEADempty. The test imports./demo-staff.jsfromsrc, so no rebuild was needed for the mutation to reach it.Acceptance notes
demoStaff.git grep -c demoStaff -- examplesanswers 0 files on8e1dde389(control:git grep -l "positions:" -- examplesanswers 9), so no example is pinned. The consumers are hotclm ([Decision]pnpm demo开箱即用时没有任何账号能打开「我的合同」——要不要让脚本建账号,与 #11 第一问耦合 hotclm#28 and its PR pnpm demo creates its demo staff, so every persona opens 我的合同 out of the box hotclm#171, whose README keeps the manual Set Manager step for this alone) and HotCRM's sales managers.defineStacksees.demoStaffis refused insidepackages[].manifest, so the list is the stack's own top level. UndercomposeStacks, each input was alreadydefineStack-ed, so an entry naming a manager that only another publisher declares is refused at its own stack. The card asks for the same list, and nothing composes demo staff across publishers today.MAX_MANAGER_CHAIN_DEPTHlives in plugin-auth, outsidepackages/spec, so the load-time check counts no links. At the boot the set-manager write walks upward from the proposed manager only: declared top-first, the link that crosses 20 is refused and reported on the subordinate; declared subordinate-first, each link is written while the chain above it is still short, and a longer chain lands.authSystemWriteContext()answers{ isSystem: true }with noattributedUserId; the write is a system write, as the account creation itself is.skills/objectstack-platform/SKILL.md(ceiling 5997 tokens) andskills/objectstack-platform/evals/config-plugins-ops.json(ceiling 1469) sit at zero headroom inscripts/check-skills-token-ratchet.mjs, and raising a ceiling is not this card's call, so the teaching line (managernames another entry's email;os devlinks it and never overwrites one set by hand) and an eval case are owed as a follow-up. The reference page is regenerated here.Verification
Derived local gates: 120 of 120 run, all exit 0 (dispatch-gates --ran reconciled)
node scripts/pm/dispatch-gates.mjs --commandsderived 112 families on8e1dde389; run serially with no build in flight: 103 exit 0, 7 exit 3 (prerequisite:packages/spec/distwas older than the last source edit, plus the two whole-repo built-output gates), 2 exit 1:check:engine-double-contract:demo-staff.test.ts now pins 2 engine double(s), ledger records 1(coverage grew). Fixed with--write, committed ina516ef709.spec check:generated:api-surface/stale against the stale dist. Afterpnpm --filter @objectstack/spec build, it is green with no regenerated file.check:engine-double-contract,spec check:generated,check:api-surface,check:browser-reachable-entries,check:dual-source-exports,check:entry-nameability,check:exported-any,check:skill-examples,check:dual-build-cjs-loads).a516ef709added 8 families to the derivation (check:agent-test-spelling,check:bash32-floor,check:cli-command-ids,check:entry-guard,check:parse-guard,check:pnpm-filter-targets,check:pnpm-flag-spellings,check:scripts-symbol-anchors): all run, all exit 0.✓ dispatch-gates --ran: 120 derived famil(ies) accounted for — 120 run, 0 NOT-MEASURED.pnpm --filter @objectstack/spec test:repo(no test in that project changed) and the repo-widepnpm lint.Human Minimum
packages/specholds no runtime logic.pnpm lintis CI's).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzehKYBxrrAG5Jid68s
Generated by Claude Code