Skip to content

feat(spec,plugin-auth): a demoStaff entry names its line manager, and os dev links it - #26763

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-9ycve3
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-9ycve3

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

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。
  • 为什么改:新起的 demo 里每个账号都没有直属上级,审批流的 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). A demoStaff entry gains one optional key, manager: the email of another entry of the same demoStaff list, 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 catches manager_id, one probe) and supervisor are aliases that point to manager.

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 its manager_id column 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 from collectDemoStaffErrors). defineStack (and so os validate and every load) refuses, with STACK_CROSS_REFERENCE_INVALID 422, each of:

  • a manager that is the address of no entry;
  • an entry that names itself (case-insensitive);
  • managers that form a loop, reported once per loop with every member and its 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). provisionDemoStaff keeps its first pass unchanged and adds a second one, linkDemoStaffManagers, after every entry has had its turn and still on the same kernel:ready, so before the application seed. For each entry with a manager:

  • no account for the entry: skipped, because the first pass already reported why it has none;
  • no account for the manager: reported on failed under the subordinate's address, naming whether the manager's entry was not provisioned or is not an entry at all;
  • the account's manager_id already equals the declared manager: nothing written;
  • the account already has a different manager: left as it is and listed on the new managersKept (an info line on the boot log names them);
  • no manager: written through applyUserManagerLink, the one derivation behind POST /api/v1/auth/admin/set-user-manager. Its refusals (self, cycle, depth, cross-organization, directory-owned) apply unchanged and are reported with their code and reason; this module carries no copy of them.

DemoStaffOutcome gains managersLinked and managersKept. auth-plugin.ts adds 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_user that nothing else reads, for the one case where an operator wants a demo account to have no manager at all and also kept manager in the stack.

Reference docs. content/docs/references/system/demo-staff.mdx regenerated (gen:docs), authorable-surface/system.json gains system/DemoStaff:manager.

Why

A fresh demo's approval ladder starts at the requester's manager, and manager_id was 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 write manager_id, so the application could not declare it.

Tests

Runs through scripts/pm/os-verify-lock.sh. Source as of 8e1dde389; head a516ef709 adds only the engine-double ledger line.

Spec: src/stack-demo-staff.test.ts (9 new cases) and the full suite
  • New describe('manager — …'): accepts a manager declared after the entry, case-insensitively; accepts a three-link chain that reaches a top; refuses a display name at demoStaff.0.manager with the message; refuses reportsTo / managerEmail naming manager; refuses a manager naming no entry (STACK_CROSS_REFERENCE_INVALID 422 + 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 test on 8e1dde389's parent tree: Test Files 1 failed | 730 passed (731), Tests 1 failed | 21928 passed | 1 todo. The one failure was src/shared/alias-integrity.test.ts: my managerId and manager_id aliases share the probe managerid. Fixed by dropping manager_id (commit 8e1dde389); re-run of alias-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, new describe('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, one manager_id update); 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 (real ObjectKernel, ObjectQL on in-memory SQLite, real AuthPlugin + SecurityPlugin), new cases 8 and 9: a fresh boot leaves manager_id set 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'], no warn). 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, which examples/basic-usage.ts imports): 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, blob 5ed3b373be04 → 7c15b3750852; × a re-run leaves a manager set by hand as it is, and says so, Tests 1 failed | 19 passed; restored, blob == HEAD and git diff HEAD empty. The test imports ./demo-staff.js from src, so no rebuild was needed for the mutation to reach it.

Acceptance notes

  • No example app declares demoStaff. git grep -c demoStaff -- examples answers 0 files on 8e1dde389 (control: git grep -l "positions:" -- examples answers 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.
  • The manager resolves within the list one defineStack sees. demoStaff is refused inside packages[].manifest, so the list is the stack's own top level. Under composeStacks, each input was already defineStack-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.
  • The 20-link depth cap does not bound a declared chain. MAX_MANAGER_CHAIN_DEPTH lives in plugin-auth, outside packages/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.
  • Attribution. At boot there is no request, so authSystemWriteContext() answers { isSystem: true } with no attributedUserId; the write is a system write, as the account creation itself is.
  • Skills-docs. skills/objectstack-platform/SKILL.md (ceiling 5997 tokens) and skills/objectstack-platform/evals/config-plugins-ops.json (ceiling 1469) sit at zero headroom in scripts/check-skills-token-ratchet.mjs, and raising a ceiling is not this card's call, so the teaching line (manager names another entry's email; os dev links 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 --commands derived 112 families on 8e1dde389; run serially with no build in flight: 103 exit 0, 7 exit 3 (prerequisite: packages/spec/dist was 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 in a516ef709.
    • spec check:generated: api-surface/ stale against the stale dist. After pnpm --filter @objectstack/spec build, it is green with no regenerated file.
  • After rebuilding spec, the nine non-green ones were re-run: all exit 0 (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).
  • The ledger commit a516ef709 added 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.
  • Final: ✓ dispatch-gates --ran: 120 derived famil(ies) accounted for — 120 run, 0 NOT-MEASURED.
  • Not run locally, declared to CI: pnpm --filter @objectstack/spec test:repo (no test in that project changed) and the repo-wide pnpm lint.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; eslint on the seven changed source files exits 0 (the repo-wide pnpm lint is CI's).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzehKYBxrrAG5Jid68s


Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:system tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 27, 2026
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/spec, touching 12 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/approvals.mdx (via manager_id (literal, a string literal in linkDemoStaffManagers), /api/v1/auth/admin/set-user-manager (route, a path literal in a comment on a changed line))
  • content/docs/capabilities/approvals.mdx (via manager_id (literal, a string literal in linkDemoStaffManagers), /api/v1/auth/admin/set-user-manager (route, a path literal in a comment on a changed line))
  • content/docs/protocol/objectql/query-syntax.mdx (via manager_id (literal, a string literal in linkDemoStaffManagers))

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

  • content/docs/releases/v17/17-5.mdx (via manager_id (literal, a string literal in linkDemoStaffManagers), /api/v1/auth/admin/set-user-manager (route, a path literal in a comment on a changed line))

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 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 55 of 207 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 — 152 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 a485b5a3e28d84550e8a33bbb5cfc2d52c82ff9e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9c21e4dc9054fe1a95c93336c58a51f24d1b8efc — the merge of head 53d29e263c2573b2442cd17af01eedab1e2c8d4b into base a485b5a3e28d84550e8a33bbb5cfc2d52c82ff9e, 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 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

⚠️ 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 a485b5a3e28d84550e8a33bbb5cfc2d52c82ff9e → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a516ef70932a183ab3d4b04336ebef8381655a2b
Local-runs: none
Scope: packages/spec/src/stack.zod.ts, packages/spec/src/system/demo-staff.zod.ts + packages/plugins/plugin-auth/src/demo-staff.ts, packages/plugins/plugin-auth/src/admin-set-user-manager.ts, packages/plugins/plugin-auth/src/auth-plugin.ts, packages/spec/src/shared/alias-probe.ts, packages/spec/src/api/error-code-ledger.zod.ts, scripts/check-skills-token-ratchet.mjs

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 43731031..a516ef70 (11 files, +513/−9); the check-runs on the head. The PR body's account was read and then set aside; every judgment below is on the tree.

Gate verdicts on the head (check-run conclusions, read 2026-10-27 at the time of this record): green — Lint & Repo Gates, Governed Surface Queue Guard, Type Check · source gates (the lane carrying check:authorable-surface and check:docs), Lint · ESLint (carrying check:engine-double-contract), Check Changeset, PR-body guards, Spec property liveness, Dogfood Regression Gate, Test UI, Build Docs, Build UI Deps, Check PR Size. Still in_progress when this record was closed: Test Core (1–8/8), Build Core, Type Check · consumer gates (carries check:api-surface), Type Check · debt ledger, Type Check · workspace, Type Check UI, Build & E2E UI. The verdict below is on the tree; the seat posts it only once those required lanes report success, and a red there re-opens this record rather than being adopted.

① Derived judgments

  • Accept-set, DemoStaffSchema (demo-staff.zod.ts): one new optional key manager: z.string().email(). A key the strict shape refused is now accepted; nothing accepted before is refused now (a manager-less list parses as it did). ⇒ Metadata-protocol: added, Clause-②: yes (widening). Right.
  • Aliases managerEmail, managerId, reportsTo, supervisor → manager: each filed key is one the shape rejects and each target is one it accepts (the alias-integrity claims hold). aliasProbe (alias-probe.ts:21) lowercases and strips _ - space, so manager_id, manager_email, reports_to reach the same entries; the manager_id row that shared managerId's probe was rightly dropped in 8e1dde38 rather than kept as a dead second spelling. Right.
  • Stack check (stack.zod.ts): collectDemoStaffManagerErrors is called from collectDemoStaffErrors, which validateCrossReferences runs BEFORE its objectNames.size === 0 early return, and defineStack runs validateCrossReferences unconditionally in strict mode (the default) on the parsed result.data. Nothing conditions it; the only skip is strict: false, which skips every cross-reference check and predates this PR. "At load, never silently skipped" holds for defineStack, os validate, os build and every host config. Right.
  • Error code: STACK_CROSS_REFERENCE_INVALID is an existing ledger row (error-code-ledger.zod.ts:1576, "items name objects the stack does not define" plus the aggregate's other findings); the class StackCrossReferenceError extends StackRefusalError carries 422. A manager naming no entry is exactly a stack-level reference the stack does not define; self-reference and loops ride the same aggregate the duplicate-name and duplicate-address findings already ride. Right, and no new code was minted outside the ledger.
  • Compose: COMPOSE_KEY_DISPOSITIONS.demoStaff = 'concat'; composeStacks refuses an input not built by defineStack, and collectArtifactCrossReferenceErrors does not re-judge managers on the composed list — so a manager resolves within the list its own defineStack saw, and a cross-publisher manager is refused at its input. Matches the card ("another entry" of the same list) and the acceptance note. Right.
  • Identity key = email, folded the same way on both sides: the stack check keys indicesByEmail on email.trim().toLowerCase() and looks manager.trim().toLowerCase() up in it; the provisioning folds entry.manager the same way and resolves it through findUserIdByEmail → addressSpellings([lowercased]), the same read the first pass used to find or create every account — so if the first pass found the manager's account the second pass finds it by construction. .email() refuses whitespace, so trim() is inert; toLowerCase() is the fold the duplicate-address refusal and sign-up storage already use. Choosing the address over the card's "by name or email" is right: name is a Tier-1 self-editable label and is the SEED's key, not the account's. Right.
  • Public surface, plugin-auth: DemoStaffOutcome gains managersLinked and managersKept (required, readonly); the only producer is provisionDemoStaff, so for every reader it is a widening. Right.
  • Generated artifacts: authorable-surface/system.json (+system/DemoStaff:manager, in sorted position) and content/docs/references/system/demo-staff.mdx (+1 row in schema key order) are generator outputs; the lane that runs check:authorable-surface and check:docs is Type Check · source gates, green on this head, so they are certified regenerated, not taken on the body's word. scripts/engine-double-contract.pinned.json pinned: 1 → 2 for demo-staff.test.ts is the check:engine-double-contract --write output for the second update double the new tests add; that gate runs in Lint · ESLint, green. A pinned count growing is coverage growth, the ledger's ordinary direction.

② Semver level

Changeset .changeset/26616-demo-staff-manager.md: @objectstack/spec: minor, @objectstack/plugin-auth: minor. The kind is added, and a kind takes at least minor on every package the diff moves (Post-Task Checklist step 3); both packages move. Both declaration lines are present in the PR body and in the changeset body: Clause-②: yes (widening) and Metadata-protocol: added. Not breaking, so no ADR-0087 marker is owed. Check Changeset green. Right.

③ Boundary flags

  • open_questions: [] — none to answer.
  • Out-of-scope finding 1 (depth cap not checked at load): the dev's note says a chain longer than 20 "is refused by the set-manager write at the boot and reported, never skipped". Read against walkChain that is only true for SOME declaration orders: the walk goes UPWARD from the proposed manager, so a chain declared subordinate-first (E1→E2, E2→E3, …) is written link by link, each walk seeing only the short chain above, and a 21-link chain lands unrefused; declared top-first it is refused at the last write. No file in hits claims the cap (the TSDoc names three refusals only), so it is not a declared≠enforced defect of this diff; it is a wrong sentence in the acceptance note. Should-fix: correct the note (or carry the cap into the load-time walk, which already has the path length, once MAX_MANAGER_CHAIN_DEPTH has a home in spec — a card, not this PR).
  • Out-of-scope finding 2 / Skills-docs: follow-up owed — FILES: under ruling [Decision] pm-dispatch: which PRs owe a Skills-docs: line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B that value is a permitted form; the seat files the owed card and rewrites the line to #N before ACCEPT (pm-dispatch SKILL.md:313, review-checklist.md:18) — a seat act, not a contract-review verdict. The zero-headroom claim is TRUE on this head: skills/objectstack-platform/SKILL.md is 24005 bytes, priced 23985 → 5997 tokens = its ceiling; evals/config-plugins-ops.json is 5873 bytes → 1469 = its ceiling. One correction to the note: "raising a ceiling is not this card's call" is half right — the standing ruling 「A:先削重复,余额按实测提高」 (card skills(objectstack-platform): teach demoStaff and when to pick it over devLogins #26302, same row) is the payment path, so the follow-up card should cite it and pay in that order (cut restatements first, then raise AT the measurement). Deferral acceptable.
  • Attribution at boot: authSystemWriteContext() with no request resolves no attributedUserId and answers { isSystem: true } — the same system write the first pass's email_verified update is. Right.
  • Hand-cleared manager re-written: consistent with the module's existing idempotency, where a hand-removed position or permission-set assignment is also re-added ("only its missing assignments are added"); the declaration wins over absence only, the boot is development-only, and the count is logged. Acceptable; see ④ should-fix on the schema TSDoc.
  • Governed surfaces: none in the file list; Governed Surface Queue Guard green; 522 changed lines. Premise (a demoStaff entry carries name, email, position?, permissionSets? only) holds on 43731031.

④ Design judgment

packages/spec/src/system/demo-staff.zod.ts — holds.

  • Tried: a manager written as a display name → refused at demoStaff.N.manager by .email() with a message that names the fix. A manager written as a user id via managerId: 'usr_…' → alias to manager, then .email() refuses the id, two loud steps, no silent strip. Mixed-case address → accepted here, folded in the stack check and the provisioning identically (①).
  • Should-fix (docs, one clause): the key's TSDoc says "A manager someone already set on that account is left as it is" but not that a CLEARED one is written again — the one case an operator can be surprised by. The module header and the changeset say it; the authorable TSDoc, which an IDE shows the author, does not. Add the clause there (the describe and the mdx row need not change).

packages/spec/src/stack.zod.ts — holds.

  • Cycle detection is complete for this graph: out-degree is at most one (one manager per entry), so from any unsettled start the walk either reaches an entry with no manager, a settled entry, or revisits the current path. A revisit is a loop, reported once with path.slice(indexOf(cursor)) (the tail an entry hangs from is excluded, as the test pins); every entry on the path is then settled, so a later start reaching the same loop stops at a settled node and does not report it twice; a second, disjoint loop is found from its own unsettled start; a long loop is only a longer path. A self-reference is diverted to its own refusal before managerOf.set, so a self-loop never enters the walk. Tried the duplicate-address edge: an entry whose own email is a repeat and that names that address resolves to the FIRST occurrence (?.[0]), so it is reported as a link beside the duplicate-address refusal rather than as self-reference — both loud, acceptable.
  • Runs at load, unconditionally, in strict defineStack; status 422 under an existing ledger code (①). Messages carry no tracker number (the check:doc-authoring rule) and each names the remedy.
  • The manager-is-not-a-string skip is moot: in strict mode the check reads the parsed result.data, where manager is already a valid address.

Cited code, read to judge the above (not reviewed; for the lane seat's ACCEPT at TIER_DEFAULT):

  • demo-staff.ts: the second pass runs after the first pass's loop, inside the same provisionDemoStaff call, on the same kernel:ready hook, and AppPlugin seeds at kernel:bootstrapped — so "after every entry exists, before the seed" holds. Each entry is its own try/catch; a thrown find/update or a refusal is pushed to outcome.failed and auth-plugin.ts logs every failed row at warn — it cannot fail half-way in silence. The refusals (self, cycle, depth, cross-org, directory-owned) are applyUserManagerLink's own; the module carries no copy.
  • Should-fix (lane): linkDeps passes no logger, so membershipOrgIds's "the cross-organization screen could not read sys_member, so the link was accepted without it" warning is dropped on this path — the one degradation that module promises to say once is silent here. Pass ctx.logger through DemoStaffProvisioningDeps. Not a must-fix: the fail-open posture is the endpoint's own and the read's failure at boot is improbable.

Must-fixes: none. Should-fixes: three (the schema TSDoc clause; the depth-cap sentence in the acceptance note; the dropped logger on linkDeps).

Implemented-by: claude/project-thread-9ycve3
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: PASS

…the boot logger to the manager links

Claude-Session: https://claude.ai/code/session_01WGufzehKYBxrrAG5Jid68s
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T15:51Z

Head 53d29e263c2573b2442cd17af01eedab1e2c8d4b. Card #26616, claim 6442804755, dev report 6445286003.

  • Contract review: owed at CONTRACT_REVIEW_TIER for system/demo-staff.zod.ts and stack.zod.ts. PASS 6445518048 at a516ef70, with no must-fixes. The one commit since then, 53d29e26, lands should-fixes (a) and (c) only: the diff from a516ef70 to 53d29e26 is 4 files, +39/−4.
    • (a) The manager TSDoc now says that a cleared manager is written again on the next boot.
    • (c) DemoStaffProvisioningDeps.logger is passed into linkDeps (auth-plugin passes ctx.logger). A new unit case makes sys_member unreachable and pins that the warning lands once while the link is still written.
  • (b): the PR body's depth-cap note now says what the tree does. A declared chain is bounded only when written top-first. The same order hole in POST /api/v1/auth/admin/set-user-manager predates this PR and is filed as plugin-auth: set-user-manager's depth cap counts only the chain above the new manager, so linking bottom-up builds a manager chain past MAX_MANAGER_CHAIN_DEPTH (each link 200) #26775 (domain:server).
  • Lane seat (rest of the diff, TIER_DEFAULT):
    • manager names another entry by email, folded with trim().toLowerCase() on both the stack and the provisioning side.
    • The stack refuses a manager that names no entry, a self-reference and a loop with STACK_CROSS_REFERENCE_INVALID (422), at load.
    • The second provisioning pass runs after every entry exists, through applyUserManagerLink. It writes only a null manager_id, and each entry fails loudly to warn.
    • No example app declares demoStaff.
  • Skills-docs: #26764. Both skills files are at zero headroom, and the card cites the skills(objectstack-platform): teach demoStaff and when to pick it over devLogins #26302 payment path.
  • Shape: Fixes #26616, 11 files. @objectstack/spec and @objectstack/plugin-auth are minor. Clause-②: yes (widening) and Metadata-protocol: added. check-governed-merges --pr 26763: NOT governed, 557 changed lines.
  • Gates: check-runs on this head are 42 success, 9 skipped and 0 red. mergeable_state is clean.
  • Landing: Tier S. pr_ready + automerge_enable in this act.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 15:53
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit 13e85be Oct 27, 2026
52 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-9ycve3 branch October 27, 2026 16:22
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 protocol:system size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

demoStaff: an entry cannot name its line manager, so a fresh demo's manager approval rung has nobody until someone sets it by hand

2 participants