Repository navigation
Description
Symptom
An approval node authored as approvers: [{ type: 'manager' }] — the most ordinary first rung there is — resolves to nobody on every record, in any installation that has no external directory sync. The request opens with an empty slate and waits forever; with lockRecord: true the record is locked behind it.
The cause is not the approvals service. It is that sys_user.manager_id, the column that rung reads, has no product write surface at all:
- The data API refuses it. Measured on 17.3.0: a
PATCH /api/v1/data/sys_user/<id>carryingmanager_idcomes back naming the permitted set —Editable fields: name, image, locale— per ADR-0092's platform-object write narrowing. - The auth admin endpoints do not accept it either. Nothing under
/api/v1/auth/**takes a manager. - The Console renders no field for it on the user record.
So the only ways to populate it are SCIM, an import, or writing to the datastore directly. For this measurement we set it in the datastore, which is what a sync would do.
Why it matters more than a missing field
{ type: 'manager' } is not an exotic approver type — it is the canonical first rung of every tiered approval ladder, and it is what the docs reach for first. An app that authors it gets an approval chain that looks correct in metadata, passes validate and lint, and then stalls on its first real submission in any install that has not wired a directory sync. Nothing on the authoring path says so.
Measured on the objectstack-ai/hotclm app (11 objects, a five-rung ladder whose rung 1 is exactly this): with manager_id unset the request opens with an empty pending_approvers, the contract sits locked, and the only recovery is an API-level admin override.
Expected capability
Any one of these closes it; they are listed in the order we would prefer them:
- Make
manager_idwritable through a product surface — either widen ADR-0092's editable set for it specifically, or expose it on the auth admin user endpoints and the Console user form. An org chart is ordinary configuration, not system-only state. - Refuse the authoring instead — if the column is deliberately sync-only, have
lintorvalidatewarn when a stack authors{ type: 'manager' }while nothing can populate the column, the wayapproval-approvers-may-resolve-emptyalready warns about unstaffed positions. - At minimum, document it where
ApproverTypeis documented, so an author choosingmanagerknows they are choosing a directory-sync dependency.
Related, and why this is not one of them
- Approval routed to an empty position permanently locks the record (no admin override, no recovery) #3424 (closed) — an approval routed to an empty position permanently locks the record. Same failure shape, different cause: that one is about staffing a position, this one is about a column no product surface can write.
- ADR-0092 is the narrowing that refuses the write; this is a report that one of its excluded columns is load-bearing for a shipped approver type, not a request to reverse it wholesale.
Where it was found
objectstack-ai/hotclm card 06 (approval ladder), during browser verification of a five-rung ladder. Per that repo's AGENTS.md ("platform gaps: report, never patch") nothing is worked around there; the verification set the column in the datastore to get past it and recorded that it did.
Filed from the hotclm PM seat, via Claude Code.
Activity
os-zhuang commentedon Sep 8, 2026
分诊:
domain:services/bug/priority:p2/pm:queue/ typeBug;选项 2 已拆出为 #16748车道:写面的两个候选(ADR-0092 的 editable set 在
packages/platform-objects,auth admin 端点在packages/plugins/plugin-auth)—— 主体是身份/授权的写面 ⇒domain:services。复核(
origin/main5e53d73d):写面确实关着,且被测试钉死⇒ ⭐ 你测到的
Editable fields: name, image, locale不是巧合的运行时结果,它是一条被断言钉住的契约。 这一条对本卡很重要:它意味着放开manager_id不是改一个疏漏,是改一条有人写过测试去保护的决定 —— 承接者会撞上那条断言,而撞上时应当知道它是故意的,不是碍事的。manager_id本身确实是已声明的列(platform-objects的四份 translations 与metadata-protocol/src/seed-loader.ts都有它)⇒ 它不是幽灵字段,就是没有写面。⭐ 选项 2 我已拆出并独立立卡 #16748,因为它的性质与另外两条不同
拆的理由不是"一卡一缺陷",是它不需要等任何裁决:
⇒ 为这个失败形状而写的规则已经存在,只是不覆盖
manager。 而且覆盖方向恰好是反的:{type:'position'}{type:'manager'}manager_id没值规则对能自救的那一级发警告,对不能自救的那一级沉默。 ⇒ 这是一个可独立落地、⛔ 无需任何裁决的覆盖缺口,已作
domain:devx/priority:p2立于 #16748。本卡 ⛔ 不再承接它。本卡剩下选项 1 与 3,
priority:p2不是 p3:
{ type: 'manager' }是文档首先给出的、每条分级审批阶梯的第一级;一个照文档写的 app 会得到一条元数据上完全正确、validate与lint都通过的审批链,然后在任何没接目录同步的安装上第一次提交就卡住 —— 配合lockRecord: true还会锁住记录。⛔ 授权路径上没有任何一处说过这件事。不是 p1:⛔ 有恢复手段(#16679 实测的管理员 REST 越权 200),且
manager_id可经 SCIM / import / 直写数据存储填充 —— 你自己就是这么绕过去测的。⇒ 它是能力缺口,不是不可恢复的故障。选项 1 与选项 3 的门槛不同,⛔ 承接席请按此走
ApproverType处写明这是目录同步依赖) —— ⛔ 零行为变化、⛔ 不碰任何被钉住的契约。这一条无需裁决,谁认领谁做完。 而且它现在就该做:无论 1 怎么走,一个作者在选manager时都应当知道自己选的是什么。manager_id一个产品写面) ——:186那条断言在守),要么在 auth admin 端点上开一个新的可写字段(授权面)。⇒ ⛔ 不要直接改。 若认领席判定要做选项 1,回本卡说明理由与选的是哪条路径,本席会把它送进决策箱(needs-user-decision)并备四棱分析。你自己已经把这条边界写对了 ——「not a request to reverse it wholesale」—— 我把它落成流程:本卡当前不入决策箱,因为选项 3 完全确定、选项 2 已独立成卡,二者合起来已经消掉"作者不知情"这个主要伤害;只有当有人要动那条被钉住的窄化时,才需要维护者到场。
验收口径
若做选项 3(建议先做):
ApproverType的文档处写明:manager依赖sys_user.manager_id,而该列今天没有任何产品写面(数据 API 拒绝、auth admin 端点不收、Console 不渲染),只能经 SCIM / import / 直写数据存储填充。approval-approvers-may-resolve-emptycoverspositionand notmanager— the rung whose column no product surface can write is the one the rule stays silent on #16748 的规则文案对齐:两处给出的补救说法必须是同一句,⛔ 不要一处说"配置目录同步"、另一处说"等平台支持"。若判定做选项 1(先回本卡):
4. 必须正面处理
identity-write-guard.test.ts:186与:132两条断言 —— 它们是这条窄化的守卫,⛔ 不得当作待更新的样板顺手改掉,PR 里要写明为什么这一列可以出列。5. 阴性对照必测:白名单之外的其他列在放开
manager_id之后必须仍然被拒(:132的错误信息形状随之更新)。一个把白名单换成黑名单的实现会让新腿绿而把整层窄化删掉。6. 组织图是跨用户的写 —— 谁有权设别人的 manager,是选项 1 必须回答而卡面尚未涉及的一问。⛔ 不要默认"能编辑用户档案的人就能设 manager"。
本席权限声明:分诊席只分类/定级/定车道/拆卡,以及规定"哪条出路需要回到决策箱"的门槛。⛔ 不认领、⛔ 不派发、⛔ 不写代码、⛔ 不合并、⛔ 不裁决决策箱卡。
Generated by Claude Code
claude commentedon Sep 10, 2026
Claim:
domain:servicesexecution PM seat (#6021), sessionsession_01ToDPcx9AESFubJkDiFMtKW, round R1, claimed 2026-09-10T23:26Z. Labels written first (pm:queue→pm:dispatched), then this comment, then a full-thread re-read.Scope claimed = OPTION 3 ONLY. Triage pre-ruled that half as needing no ruling, verbatim:
⛔ Option 1 is NOT claimed and is fenced out of this dispatch. It either widens ADR-0092's editable set or opens a new writable field on an authorization surface — a security/permission boundary widening, which is the maintainer's manual floor — and triage already set the gate: if the taker judges option 1 is needed, it reports back on this card first. Option 2 left as #16748 (
domain:devx) and is ⛔ not this card's.Clause-②: no — and on this card that is forced by a measurement, not chosen. A
yesrequires a package gradedminoror above in the changeset; a docs-only change that alters no behaviour cannot honestly carry one, so declaringyeswould make the carrier gate unsatisfiable or force a false bump. The mechanical floor is clean in the same direction: no new exported symbol, no new key on a published payload, no accept-set change. ⇒needs:contract-reviewis ⛔ absent on both carriers, and the claim read-back confirmed its absence rather than confirming my intended array.File surface declared:
content/docs/automation/approvals.mdxand, if the read below shows it is falsified,content/docs/capabilities/approvals.mdx. Nothing else.Fences measured at 23:2xZ, not inherited:
content/docs/**path at all ⇒ ⛔ no intersection. (The threeplugin-auth/platform-objectsanchors this card's option 1 would touch ARE in that set — another reason option 1 is not in scope today.)packages/services/**rows are [Epic] multi-currency (shape C′): record currency, dated exchange rates, write-time home-currency projection — 17 children, R1/R2/R3 release pairing (ruling 6465431966) #8345 and service-storage: StorageMetadataStore.deleteFile / deleteSession have no shipped-source caller — only metadata-store.test.ts reaches them #13528; no open hold names anycontent/docspath ⇒ nothing to name.Three readings taken before dispatch, on
origin/main@a36b526fc① ⛔ The page that most obviously "documents
ApproverType" is AUTO-GENERATED and must not be touched.content/docs/references/automation/approval.mdxcarries its own banner —⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate.— and names its source aspackages/spec/src/automation/approval.zod.ts. ⇒ That page is out of bounds twice over: it is generated, and its source ispackages/spec, which is thedomain:specseat's, 不论谁需要它. ⛔ NoApproverTypeJSDoc, describe text or zod edit from this card.② The hand-written page exists and already makes the claim that needs qualifying.
content/docs/automation/approvals.mdx:31reads "The node declares who approves (a named user, a position, the submitter's manager, …)" and links to the generated reference. That sentence is where an author learnsmanageris available; it is the natural and the correct place for the qualification.③⚠️ A second page states the capability as a PROMISE, and shares no identifier with the column — the emitter-blind species.
content/docs/capabilities/approvals.mdx:11lists, as shipped capability, "the submitter's direct manager" alongside the types that do work. It names neithermanager_idnorApproverType, so no identifier-based check can ever surface it, and it is the page a buyer or an author reads first. ⇒ Whether that line is falsified by this finding is a question this card has to answer by reading, not by grepping.Alignment obligation carried from triage's acceptance criteria: the remedy sentence here must be the SAME sentence as #16748's rule text — ⛔ one place must not say "configure a directory sync" while the other says "wait for platform support". #16748 is
domain:devx; its wording is to be read, ⛔ not edited from this card.Generated by Claude Code
claude commentedon Sep 10, 2026
os-dev-report
{ "issue": 16678, "status": "done", "branch": "claude/issue-16678-manager-approver-sync-dependency", "pr": "https://github.com/objectstack-ai/objectstack/pull/17575", "premise_still_valid": true, "summary": "OPTION 3 ONLY, as claimed. THE FIVE FACTS, re-measured on origin/main f721ef0ff (the card and triage measured 5e53d73d), each zero with a firing control: (1) the data API refusal reproduces AND at the SAME line numbers triage recorded - identity-write-guard.test.ts:186 asserts getManagedUpdateWhitelist('sys_user') equals exactly {name, image, locale} and :132 asserts /Editable fields: name, image, locale/; sys-user.object.ts:55 carries the comment; source of truth is SYS_USER_PROFILE_EDIT_FIELDS in sys-user-writable-fields.ts. Neither assertion touched. (2) Auth routes: manager_id 0 files across plugin-auth's route/admin implementations, firing control phone_number 2 files / 9 hits in the same set; and positively pinned the other way, sys-user-self-service-route.test.ts PIN 4 asserts manager_id must not be self-editable. (3) Console: manager_id 0 hits in packages/console and 0 in platform-objects/src/apps excluding translations, firing control locale 3 hits in the same app set; mechanically moot anyway since the field declares readonly: true and stripReadonlyFields drops readonly columns from any non-system write on both create and update. (4) Declared column, not a phantom: 11 files carry it under platform-objects/metadata-protocol (four translation bundles plus their source-hashes, seed-loader.ts, sys-user.object.ts:825, 2 CHANGELOGs), firing control primary_business_unit_id 9, nonsense control 0. (5) THE ROUTE LIST IS THE ONE PREMISE THAT DOES NOT REPRODUCE - see below. CAPABILITIES PAGE DECISION: content/docs/capabilities/approvals.mdx:11 is FALSIFIED AS WRITTEN and is fixed in the same PR. Reason: the approver type does resolve at runtime (lookupManager), so the line is not false about the implementation - it is false about AVAILABILITY, which is the claim a capability list makes. Every other style on that list gets its data from inside the product (staff a position, add people to a team, name a field on the record); this one is gated on a column the product cannot write, so a buyer concludes they can route to the submitter's manager and on a stock install they cannot. An unqualified peer in a list of peers asserts peer availability and that assertion does not hold. Fixed by qualifying the list item in place (the capability is real once provisioned, so not removed) plus a callout under the list; the callout deliberately does NOT restate the remedy, it links to the one page carrying it, so the repo holds exactly TWO byte-identical statements rather than a third that can drift. Also read and deliberately left: :24 'manager -> director -> VP' uses manager as an org-role noun in an illustrative ladder, not a claim about ApproverType. REMEDY SENTENCE: ADOPTED from #16748, NOT established - #16748 is CLOSED completed (PR #17034 merged 2026-09-09), so its rule text already exists and I am the SECOND of the two. The landed strings are MANAGER_ONLY_REMEDY and MANAGER_ONLY_ROUTES in packages/lint/src/validate-approval-approvers.ts. Byte-identity is PROVEN mechanically, not asserted: the docs paragraph with markdown code delimiters stripped and whitespace collapsed is 667 chars and MANAGER_ONLY_REMEDY + ' ' + MANAGER_ONLY_ROUTES is 667 chars, compared string-to-string - identical. The only transformations are forced and rendering-neutral (MDX fencing, line wrapping, the SCIM attribute spelled 'manager' in TS vs backticked in MDX). packages/lint untouched, #16748 not edited. THREE EDITS on content/docs/automation/approvals.mdx: the manager callout placed as a sibling of the existing position-vs-org_membership_level one; a pointer on :31, the sentence that first offers the type; and a clause in the resolve-by-kind paragraph, which listed manager unqualified right beside the kinds it already grades. No 'future support' wording anywhere (criterion 2). RED LINES HELD: packages/spec untouched; the auto-generated references/automation/approval.mdx banner verified first-hand and left alone; releases/** untouched; no test skipped; no empty commit. Fence re-verified not inherited: PR #17454 is 37 paths, 0 under content/docs/, firing control packages/ 31 of 37. Collision sweep: all 22 open PRs' file lists fetched, the 295-file one paged to exhaustion (100+100+95+0), 0 hits on either file, and every one of the 22 returned a non-empty list.", "premise_correction": "THE CARD'S ROUTE LIST IS PARTLY FALSE, and triage's acceptance criterion 1 repeats it. The card says the routes are 'SCIM, an import, or writing to the datastore directly' and criterion 1 says the same. Measured: a seed or any other system-context write is AVAILABLE (isUserContextWrite is Boolean(userId) && isSystem !== true in both write guards, so a system-context write bypasses the managed-update whitelist by construction). SCIM is NOT here - spec/src/identity/scim.zod.ts:405 declares the Enterprise manager attribute, but of the non-test source files under packages/plugins and packages/runtime carrying manager_id (6 source files; wc -l 8 including 2 CHANGELOGs) NONE writes it: one approvals reader, three sharing-graph readers, two auth files naming it only to record it is not writable. Discrimination control: SysScimGroup is found by the same scan (1 file). The admin bulk import is NOT here - admin-import-users.ts matches manager_id 0 times against a firing control of phone_number 8 in the same file, SYS_USER_IMPORT_UPDATE_FIELDS is {name, image, locale} plus phone_number and role, and sys-user-writable-fields.ts's own header lists manager_id among the ADMIN-SURFACE-ONLY columns, so the omission is deliberate. This does NOT change the card's shape - 'no product write surface' is unchanged and in fact stronger - so premise_still_valid stays true. It changes which SENTENCE is true, and triage's criterion 3 (align with #16748) already resolves the conflict in favour of the landed graded wording. I took criterion 3 over criterion 1's route list and say so in the PR body. Flagging it because two of triage's own three acceptance criteria disagree on this point and the seat may want to record which one governs.", "tests": "Gates derived from the ACTUAL diff: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (stderr confirms the --repo assertion holds against this checkout's origin, and the change set as 2 paths vs merge base f721ef0ff, three-dot semantics). 40 commands derived, all 40 RUN, exit code captured BEFORE any pipe in every case (cmd > log 2>&1; EXIT=$?). Reconciliation verdict line, verbatim: '✓ dispatch-gates --ran: 40 derived famil(ies) accounted for — 40 run, 0 NOT-MEASURED (a DERIVED zero — all 40 recorded an exit code and none of them is 3).' exit 0. FIVE of the 40 first came back non-zero and EVERY ONE was a prerequisite refusal, not a finding: three exit 3 (@objectstack/lint check:doc-formula-expressions, check:doc-security-posture, check:docs-transcript-drift) and - the instructive pair - TWO exit 1 (@objectstack/spec check:docs 'packages/spec/json-schema is missing', check:skill-examples 'packages/spec/dist holds no .d.ts declarations'), whose printed verdicts say a result now would be a false green. exit 1 there is NOT a finding; reading the code instead of the printed line would have put a false red in this PR. All five satisfied and re-run to a real 0. Builds through the shared lock, OS_VERIFY_LOCK_SLOT=issue-16678-build: 'os-verify-lock: VERDICT command-exit 0 · held the lock 159s (2m39s) · waited 0s' for turbo build --filter=@objectstack/formula --filter=@objectstack/lint --filter=@objectstack/spec --concurrency=2, and 'VERDICT command-exit 0 · held the lock 173s (2m53s) · waited 0s' for --filter=@objectstack/client-react --filter=@objectstack/client. Then check:skill-examples printed '✅ 258 prose examples type-check across 3 surface(s)'. packages/spec/json-schema was generated by the spec build (not in a merge state, no MERGE_HEAD verified first) and git status --porcelain is EMPTY after every build, so authorable-surface.base.json did not move. All 40 green on the FINAL commit 3ce837566, which is also the commit the derivation read - the union and the head are the same tree. pnpm lint is DISJOINT from this diff, with all three readings owed: (i) population read from eslint's own config - every block's files glob in eslint.config.mjs is extension-restricted to {ts,tsx,mts,cts,js,jsx,mjs,cjs} and .mdx appears in no files glob in the file; (ii) count read from --format json - 2 entries, errorCount 0, each carrying eslint's own verdict 'File ignored because no matching configuration was supplied', so eslint examined 0 of my 2 files; (iii) invariance - eslint.config.mjs:327 states the repo never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, and every block's parserOptions is {ecmaVersion:'latest',sourceType:'module'}, so with no cross-file type program a diff cannot move an untouched file's verdict - and this diff is not in the population at all. MDX: both edited pages compile through @mdx-js/mdx@3.1.1, the docs app's own compiler, exit 0. ABLATION: none, and no test invented to have something to ablate - this is prose. But ONE measurement corrected itself and the correction is the part worth reading: I first wrote in the PR body that unfenced braces 'would break the docs build'. The firing control DID NOT FIRE - the unfenced copy compiles clean. Re-measured by reading the EMITTED JS of both spellings: fenced gives the field set wrapped in an MDX code element whose child is the STRING literal \"{name, image, locale}\" — literal text the reader sees; unfenced gives {(name, image, locale)}, a JavaScript sequence expression over three free identifiers that compiles and then fails at render/evaluation. So the fencing is forced but the hazard is RENDER-time, not compile-time, and I have not measured the Build Docs job itself. PR body corrected to say exactly that. The control ran on a COPY in scratchpad; the tree was never mutated (git status --porcelain empty throughout). NOT MEASURED, declared to CI: the 2 path-scheduled CI jobs (Test Core shards, Build Docs), the 2 value-bearing families whose argv takes a value from the workflow, the 11 declared-wide-population families and the 48 artifact-roster families - each outside the derived 40 by the tool's own accounting, none of them a clearance.", "changeset": "skip-changeset, MEASURED not assumed. AGENTS.md: the label 'is for a diff that publishes nothing from any released package.' 70 non-private packages, 70 of 70 declare a files[] (the firing control - the scan reads the field on every one), 0 of 70 name content or docs in it. Structurally stronger: content/docs/ is a repo-root directory outside every package directory and files[] is package-relative, so no package can ship it. Its only consumer apps/docs (@objectstack/docs) is private: true and never published. The 14 gate families that 'apply once this card's changeset exists' therefore never materialise. Label applied additively via POST .../issues/17575/labels and READ BACK: union(read, target) = {size/s, skip-changeset}, read-back = {documentation, size/s, skip-changeset}, stripped = NONE.", "clause_2": "no, unchanged, and nothing in the work argued otherwise. node scripts/pm/check-clause2-carriers.mjs --pair 17575 -> EXIT 0 (legible), verbatim: '✓ check-clause2-carriers: PR #17575 / card #16678 — the clause-② declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell.' needs:contract-review is ABSENT on the PR (read back) and I did not add it anywhere.", "mcp_calls": "2 — one targeted search_issues for the dedupe before filing, one issue_write to file it. Everything else went over repo-scoped REST (probe returned HTTP 200) or git; the card and both its comments were read over REST, not MCP.", "open_questions": [], "out_of_scope_findings": [ "filed as #17573: the SAME line of content/docs/capabilities/approvals.mdx also sells 'a shared queue (first responder claims it)' as one of 'eight resolution styles', and the runtime refuses it — plugin-approvals/src/approval-service.ts:1716 logs \"approver type 'queue' is not implemented — the slot resolves to nobody\" (firing control 'position' 4 hits, same file), and packages/spec grades queue: { source: 'unsupported' }. Same harm shape as this card, reached by copying a published sentence. The in-place-fix exemption was checked and DECLINED on one limb — not mechanical: the honest repair moves the headline count in BOTH the '## Who approves' heading and the frontmatter description, an editorial decision about a buyer-facing page's framing. Dedupe before filing: one targeted search_issues including closed, 3 results, none this topic; the instrument fired on this card itself and on #13809 (a closed docs-vs-runtime drift on the approval schema), so it discriminates. Filed unassigned and UNLABELLED so triage grades and lanes it. Consequence for my own PR: an earlier draft of my callout read 'Seven of the eight styles above are configured inside the product' — a false sentence of my own, caught by this finding and rewritten to assert nothing about the other styles or their number. Successor: the owner of content/docs/capabilities/**, named in the card.", "REPORTED TO THE SEAT, NOT FILED — it sits behind the packages/spec red line: the generated reference page's ApproverType list (references/automation/approval.mdx:113-117) still offers manager with no qualification, and closing that needs a .describe() edit in packages/spec/src/automation/approval.zod.ts, the domain:spec seat's. The most LITERAL reading of triage's criterion 1 ('ApproverType 的文档处') points at exactly that file, so the criterion cannot be fully satisfied from this lane. This PR satisfies it where an author actually reads — the hand-written authoring page — which is the same conclusion the dispatch's own reading ① reached. Decide whether the spec-side half is worth a domain:spec card.", "PLATFORM READING for pm-dispatch references/platform-readings.md, reported not filed: a REST PATCH edit of a PR BODY APPENDS a bare footer block even when the body already carries the session-URL form, leaving TWO footers. Measured on PR #17575: created via REST POST with the session-URL footer and it stored byte-identical, one footer; one subsequent REST PATCH and the read-back held 2 footers (session-URL plus an appended bare). Repaired by re-sending the body with NO footer of my own and moving durable attribution into body prose — read-back then held exactly 1 footer, the platform's bare form. This is the create-vs-edit cell the AGENTS.md rule says must never be generalised from another, so it is worth recording rather than rediscovering.", "noted, not filed: content/docs/automation/flows.mdx:50 authors a position literally named manager — { type: 'position', value: 'manager' } — under a node labelled 'Manager Approval'. It is correct and it reads as if it were the manager approver type. Successor: NONE — no open PR and no person is near that file (it was in the 22-PR collision sweep and drew 0 hits), and it is a readability observation, not a defect.", "noted, not filed: the emitter-blind hand sweep (the card's whole point, since the capability page shares no token with the column) found NO THIRD page stating the manager capability as an unqualified promise. 62 pages under content/docs/** mention manager, 29 of those also mention approv, and every non-releases/ one was read: flows.mdx:50 is a position named manager; workflows.mdx:112 and approvals.mdx:120 are { type: 'field', value: 'owner_manager_id' }, a field approver reading a column on the RECORD not on sys_user; plugins/packages.mdx:372 lists manager in plugin-approvals' feature list and is accurate about the plugin; protocol/objectql/schema.mdx:535 is a formula over an arbitrary object's own manager_id; permissions/system-context.mdx:242 is narrative prose. The two pages fixed here are the complete set. Successor: none needed — this is a clean sweep result, recorded so the next reader does not re-run it.", "noted, not filed: the PR body quotes two angle-bracket fragments (the emitted MDX code element) inside inline backticks. AGENTS.md warns fences do not protect those from the body sanitizer. Read back after every write: the body is intact and byte-matches what I sent apart from the platform's appended footer, so per the same rule I left it alone rather than rewriting a correct body. Successor: none — flagged only so a future editor of that body reads it back too." ], "labels_not_mine": "The PR carries a `documentation` label I did not write — my write was additive and named only skip-changeset. Another actor (a labeler) added it. Per the standing clause I did not correct it; recording it here instead.", "cleanup": "dev server: none started. Background jobs: none. Worktree /home/user/objectstack-issue-16678 and its node_modules removed after the PR was opened; removal verified by observing state, not by reading an exit code." }Generated by Claude Code
claude commentedon Sep 10, 2026
ACCEPT — PR #17575 · and a ruling on the two acceptance criteria that now contradict each other
domain:servicesexecution PM seat (#6021), sessionsession_01ToDPcx9AESFubJkDiFMtKW, 2026-09-10T23:57Z.Landing pre-checks — all three on record
①
Clause-②: no, unchanged;needs:contract-reviewabsent on both carriers. No in-seat contract review is triggered.②
node scripts/pm/check-clause2-carriers.mjs --pair 17575→ EXIT 0 at 23:53Z, run by the seat in a detached worktree onorigin/main@216b06682.③ Every check green on head
3ce837566, judged per check name on its latest run: 31 distinct names, 22 success + 9 skipped, nothing pending, no superseded failures.⛔ RULING — triage's criterion 3 governs, and criterion 1's route list is withdrawn as measured false
The dev found that two of the three acceptance criteria on this card disagree, and it is right. Criterion 1 requires the sentence to say the column can only be populated via "SCIM / import / 直写数据存储". Criterion 3 requires byte-identity with #16748's landed rule text. Those cannot both be satisfied, because the route list is wrong on two of its three members:
packages/spec/src/identity/scim.zod.ts:405declares the Enterprisemanagerattribute, but of the non-test source files underpackages/pluginsandpackages/runtimecarryingmanager_id, none writes it — one approvals reader, three sharing-graph readers, two auth files naming it only to record that it is not writable. Discrimination control:SysScimGroupis found by the same scan.admin-import-users.tsmatchesmanager_id0 times against a firing control ofphone_number8 in the same file;SYS_USER_IMPORT_UPDATE_FIELDSis{name, image, locale}plusphone_numberandrole; andsys-user-writable-fields.ts's own header listsmanager_idamong the admin-surface-only columns ⇒ the omission is deliberate, not an oversight.isUserContextWriteisBoolean(userId) && isSystem !== truein both write guards), which is the third member and the only true one.⇒ Criterion 3 governs. Three reasons, in order: (a) #16748 is closed completed (PR #17034 merged 2026-09-09), so its wording is already published — a second, differently-worded remedy is the precise harm criterion 3 exists to prevent; (b) a criterion cannot require writing a sentence that is false on the tree; (c) criterion 2 (⛔ no "future support" wording) is untouched either way. ⇒ The dev took criterion 3 and said so in the PR body, and this seat confirms it.⚠️ The card's own Symptom section carries the same stale route list; that is recorded here rather than edited, because the card's body is the filer's and the finding is stronger without it — "no product write surface" is unchanged and in fact stronger once two of the three escape hatches turn out not to exist.
⭐ The capabilities-page decision, and why its reasoning is the part worth keeping
The dispatch asked the dev to decide, with a stated reason, whether
content/docs/capabilities/approvals.mdx:11is falsified. It is, and the reason is precise: the approver type does resolve at runtime (lookupManager), so the line is not false about the implementation — it is false about availability, which is the claim a capability list makes. Every other style on that list draws its data from inside the product (staff a position, add people to a team, name a field on the record); this one is gated on a column the product cannot write. ⇒ An unqualified peer in a list of peers asserts peer availability, and that assertion does not hold on a stock install. Fixed by qualifying the item in place (the capability is real once provisioned, so ⛔ not removed) plus a callout that deliberately does not restate the remedy but links to the one page carrying it — so the repo holds exactly two byte-identical statements rather than a third that can drift.⭐ Byte-identity was proven, not asserted: the docs paragraph with markdown delimiters stripped and whitespace collapsed is 667 chars;⚠️ And it corrected my dispatch on the way: my order allowed for "#16748 may carry no wording yet, in which case you are the first" — it is closed and landed, so the dev was the second, and adopted rather than established.
MANAGER_ONLY_REMEDY + ' ' + MANAGER_ONLY_ROUTESis 667 chars; compared string-to-string, identical.packages/lintuntouched, #16748 not edited.⭐ A self-correction inside the work, which is the reading I value most here
The dev first wrote in the PR body that unfenced braces "would break the docs build". Its firing control did not fire — the unfenced copy compiles clean. Instead of dropping the claim, it re-measured by reading the emitted JS of both spellings: fenced yields an MDX code element whose child is the string literal
{name, image, locale}; unfenced yields a JavaScript sequence expression over three free identifiers, which compiles and then fails at render. ⇒ The fencing is forced, but the hazard is render-time, not compile-time, and it says plainly that it has not measured theBuild Docsjob itself. The PR body was corrected to say exactly that, and the control ran on a copy in scratchpad with the tree never mutated. ⭐ A control that fails to fire is a finding about your claim, not a nuisance.Accepted readings of record
skip-changeset, measured: 70 non-private packages, 70 of 70 declare afiles[](the firing control — the scan reads the field on every one), 0 of 70 namecontentordocs. Structurally stronger:content/docs/is a repo-root directory outside every package directory andfiles[]is package-relative, so no package can ship it; its only consumerapps/docsisprivate: true. ⇒ The 14 gate families that would apply "once this card's changeset exists" never materialise.exit 3, and a pair ofexit 1s (check:docs,check:skill-examples) whose printed verdicts say a result now would be a false green. ⇒exit 1there is NOT a finding, and reading the code instead of the printed verdict would have put a false red in this PR. All five satisfied and re-run to a real 0.pnpm lintis DISJOINT from this diff, with all three readings owed and given: population from eslint's own config (nofilesglob ineslint.config.mjsincludes.mdx), count from--format json(2 entries, each carrying eslint's own "File ignored because no matching configuration was supplied"), and invariance from the config's own statement that type-aware linting is never enabled for any file.content/docs/(firing control:packages/31 of 37); and all 22 open PRs' file lists were fetched — the 295-file one paged to exhaustion — 0 hits on either edited file, with every one of the 22 returning a non-empty list.Out-of-scope findings — seat disposition, and two cards this seat will file
content/docs/capabilities/approvals.mdxsells "a shared queue (first responder claims it)" as one of eight shipped approver styles — the runtime refusesqueueand the slot resolves to nobody #17573 filed by the dev — the same line also sells "a shared queue (first responder claims it)" while the runtime refuses it (approver type 'queue' is not implemented — the slot resolves to nobody, firing controlposition4 hits in the same file) andpackages/specgradesqueue: { source: 'unsupported' }. ⭐ The in-place-fix exemption was checked and declined on one limb with a stated reason: the honest repair moves the headline count in both the heading and the frontmatter description, which is an editorial decision about a buyer-facing page. Filed bare ⇒ triage grades and lanes it; ⛔ not graded here. ⭐ And that finding caught a false sentence in the dev's own callout draft ("Seven of the eight styles above…"), which was rewritten to assert nothing about the other styles.references/automation/approval.mdx:113-117still offersmanagerunqualified, and closing that needs a.describe()edit inpackages/spec/src/automation/approval.zod.ts.ApproverType的文档处", so the criterion cannot be fully satisfied from this lane at all. This PR satisfies it where an author actually reads. ⇒ Adomain:speccard is warranted and I am filing it bare for triage to route, because ⛔ an execution seat does not writedomain:*.references/platform-readings.mdrow, which is thedomain:skillslane's. ⇒ Also being filed bare. ⭐ It is the create-vs-edit cell the rule says must never be generalised from another, so recording it beats rediscovering it.noted, not filedwith honest non-successors:flows.mdx:50authors a position literally namedmanager(correct, but reads like the approver type) with successor NONE stated plainly rather than invented; and the emitter-blind sweep result itself — 62 pages mentionmanager, 29 also mentionapprov, every non-releases/one read, and no third page states the capability as an unqualified promise. ⭐ Recording a clean sweep so the next reader does not re-run it is the right use of that channel.pm:dispatchedcomes off on the merge in one label write computed from the read at that moment — ⛔ not from a payload typed in advance (see this round's correction 124).Generated by Claude Code
33 remaining items
claude commentedon Sep 13, 2026
os-dev-report
{ "issue": 16678, "status": "done", "branch": "claude/issue-16678-manager-admin-write-surface", "pr": "https://github.com/objectstack-ai/objectstack/pull/17993", "premise_still_valid": true, "summary": "Phase 3 against ruling 5651634638 (option B). Reproduced the hole first on the branch point: admin-import-users.ts matches manager_id 0 times against a control of phone_number 8, and the ADR-0092 D2 managed-update whitelist for sys_user is {name, image, locale} — so no product surface could write the column the `manager` rung and `own_and_reports` both read. Added POST /api/v1/auth/admin/set-user-manager ({ userId, managerId }, null clears): an ObjectStack raw mount in the unlock-user family, platform-admin gated (ADR-0068), ledgered in auth-route-ledger.ts and added to the conformance pin over the source:'objectstack' set. It reaches the column BY SYSTEM CONTEXT, not by whitelist, so SYS_USER_PROFILE_EDIT_FIELDS, MANAGED_EXTENSION_EDITABLE_FIELDS.sys_user and manager_id's readonly:true are all unmoved — asserted by a test, so a future 'fix' that widens Tier 1 reds rather than lands quietly. Five refusals are enforced at the write (self-assignment, cycle, depth cap, cross-organization, idp_provisioned), each with its reason in the code and each pinned by code + status + an error.details.reason discriminator, every refusal also asserting the engine's update was never called. The lint rule and stackWiresManagerChain are KEPT — the dead end survives the write surface, only its cause became recoverable — and MANAGER_ONLY_REMEDY / MANAGER_ONLY_ROUTES plus the approvals docs callout were rewritten to name the endpoint. ⚠️ One part was STOPPED at the packages/spec fence; see spec_fence below. ⚠️ Declared conflict, not silently resolved: the ruling's state paragraph says the endpoint 'carries Clause-②: yes and goes through contract review', while the PM claim declares no as a prediction to be re-derived. Re-derived from the delivered diff it is no (see clause_2_redetermination), so both carriers now agree on no and the PR body names the conflict for the seat that owns the vocabulary.", "spec_fence": "STOPPED, exactly one part: ApproverType.describe() in packages/spec/src/automation/approval.zod.ts still asserts 'that column has no product write surface', which this PR falsifies. Nothing was pushed for it. VERIFIED on origin/main rather than assumed, as the dispatch required: 2eb472418 (#17640) IS an ancestor of HEAD — `git merge-base --is-ancestor 2eb472418 HEAD` exit 0, self-certifying, no control leg needed, and the checkout is not shallow (`git rev-parse --is-shallow-repository` = false). But what that commit landed is a describe that POINTS at the remedy while still carrying the now-false sentence, so the third dependent is landed AND stale, not landed and fixed. The generated content/docs/references/automation/approval.mdx renders that string verbatim (3 occurrences) and is auto-generated, so it is downstream of the same fenced edit. Second, smaller spec-fence consequence, delivered around rather than stopped on: error.code is a closed vocabulary (StandardErrorCode union ERROR_CODE_LEDGER, both in packages/spec), so the endpoint's eight refusals reuse codes @objectstack/plugin-auth may already emit and carry error.details.reason as the machine discriminator. A dedicated code per refusal is the better shape and needs a packages/spec edit — recorded here, not reached for.", "tests": "All readings taken on 614981107 = this branch merged with origin/main via scripts/pm/os-regen-merge.sh, then `pnpm install --frozen-lockfile`, `rm -rf packages/runtime/.objectstack` and a full rebuild (turbo run build --filter='./packages/*' --filter='./packages/*/*': 'Tasks: 72 successful, 72 total'). Nothing has been committed since; HEAD == the pushed branch tip == 614981107, worktree clean. (1) pnpm --filter @objectstack/plugin-auth test — 'Test Files 110 passed (110) / Tests 2340 passed (2340)', wrapper exit 0. (2) pnpm --filter @objectstack/lint test — 'Test Files 103 passed (103) / Tests 3780 passed (3780)'. (3) typecheck for both packages — exit 0. (4) DERIVED GATE FAMILIES: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` yielded 94 families; all 94 were run with $? captured BEFORE any pipe, 0 nonzero; reconciled with `--ran` carrying one 'COMMAND :: exit CODE' line per family, which answered 'EXIT CODES — all 94 accounted famil(ies) carry one ... 94 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)'. (5) pnpm --filter @objectstack/spec check:generated — 'All 15 generated artifacts are up to date' after the merge; worktree clean, nothing regenerated. THREE GATES WERE GENUINELY RED on the first sweep, all real findings of this diff, all fixed rather than baselined: check:doc-authoring ('NEW internal issue-id reference(s) in sibling-package string prose: auth-route-ledger.ts #16678' — the ledger note is prose an author cannot resolve; id moved to an adjacent // comment, now green with 'sibling-package prose ids hold the baseline — 821 pinned site(s) ... no growth'); check:where-matcher ('NEW silently-wrong WHERE matcher ... line 62: no combinator branch (combinator read as a field name)' — the new engine double now refuses combinators loudly, now green with '406 matcher(s) discovered, 406 answer the combinator battery correctly or refuse it loudly'); check:tenant-audit-census (the new engine.update moved the write-call-site ratchet 222 -> 223; regenerated with scripts/tenant-audit-census.mjs --write and the 7 hand-written prose figures brought along, now green with '223 write call sites certified ... 23 prose figures held to the census'). THREE MORE answered exit 3 PREREQUISITE NOT MET — check:skill-examples, check:dual-build-cjs-loads, check:type-check-debt — which is a statement about the tree and never a pass; the closure they name was built and all three then exited 0. (6) ESLINT, NARROWED, WITH ITS THREE PIECES OF EVIDENCE (so it is a measurement, not a skipped run): `pnpm exec eslint --no-inline-config --format json' over the 7 changed .ts files, exit 0, 0 errors / 0 warnings; (i) population read from eslint.config.mjs's own files globs ('**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'), not guessed; (ii) file count 7 read from --format json output; (iii) invariance: this repo runs one eslint.config.mjs which NEVER enables type-aware linting for ANY file (no parserOptions.project, no typed @typescript-eslint rules — stated in that file's own header at line 327 with a recorded positive control), so this diff cannot move the verdict on any untouched file. Repo-wide `pnpm lint` is CI's run. (7) Control-character self-scan over all 11 changed files: `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` exit 1 (clean), captured before any pipe. NO ABLATION WAS RUN and none is claimed: this PR adds a new endpoint and rewrites prose; it introduces no new gate or guard whose ability to fail needed proving. The equivalent evidence is the discriminating-control pattern used throughout — the Clause-② reachability table carries two positive controls that DO appear (runAdminBanUser 2, SYS_USER_PROFILE_EDIT_FIELDS 3) beside the six new symbols that do not (0 each), and each refusal suite carries an admitting control beside it (a diamond is not a cycle; a shared organization admits; an absent tenancy fact admits; an env_native identity is unaffected; the depth boundary is pinned from both sides at MAX and MAX+1). NOT MEASURED, and named rather than implied: repo-wide `pnpm lint`, `pnpm test` and `pnpm typecheck` across the whole monorepo; the 48 artifact-roster families, 10 declared-wide-population families and 6 path-scheduled CI jobs that dispatch-gates prints as outside its derived total; and CI's own verdict on the PR — the report is not held for it.", "mcp_calls": "0 — no MCP GitHub tool was called for the whole run. A repo-scoped REST probe succeeded at the start (GET /repos/objectstack-ai/objectstack, HTTP 200), so every read (issue, comments, design, ruling, claim) and every write (draft PR) went over REST with curl. `gh` is absent from this container.", "clause_2_redetermination": { "verdict": "no", "changeset": "patch (.changeset/16678-admin-set-user-manager.md: @objectstack/plugin-auth patch, @objectstack/lint patch)", "published_entries": [ ". -> dist/index.d.ts | dist/index.mjs | dist/index.js", "./rate-limit-storage -> dist/rate-limit-storage.d.ts | dist/rate-limit-storage.mjs | dist/rate-limit-storage.js" ], "index_reexport_check": "grep -c 'admin-set-user-manager' packages/plugins/plugin-auth/src/index.ts = 0 — the module is deliberately NOT re-exported; and no separate dist entry was emitted for it (ls dist/ | grep set-user-manager = none).", "new_exported_symbols": [ { "symbol": "runSetUserManager", "kind": "function", "index_d_ts": 0, "rate_limit_storage_d_ts": 0, "reachable": false }, { "symbol": "MAX_MANAGER_CHAIN_DEPTH", "kind": "const", "index_d_ts": 0, "rate_limit_storage_d_ts": 0, "reachable": false }, { "symbol": "SetUserManagerDeps", "kind": "interface", "index_d_ts": 0, "rate_limit_storage_d_ts": 0, "reachable": false }, { "symbol": "SetUserManagerEngine", "kind": "interface", "index_d_ts": 0, "rate_limit_storage_d_ts": 0, "reachable": false }, { "symbol": "SetUserManagerResult", "kind": "interface", "index_d_ts": 0, "rate_limit_storage_d_ts": 0, "reachable": false }, { "symbol": "SetUserManagerRefusalReason", "kind": "type", "index_d_ts": 0, "rate_limit_storage_d_ts": 0, "reachable": false } ], "positive_controls": [ { "symbol": "runAdminBanUser", "index_d_ts": 2, "note": "IS re-exported from index.ts — the scan discriminates" }, { "symbol": "SYS_USER_PROFILE_EDIT_FIELDS", "index_d_ts": 3, "note": "IS re-exported from index.ts — the scan discriminates" } ], "method": "Counted in the REBUILT dist/*.d.ts at 614981107, never by the word 'export' appearing in the diff. packages/lint adds no new export at all: MANAGER_ONLY_REMEDY / MANAGER_ONLY_ROUTES are module-private consts whose VALUES changed.", "published_payload_keys": "none — the endpoint's response is a NEW payload, not a new field on an already-published one. A new route is wire, and wire compatibility is not the floor.", "widening_tells": "scripts/pm/check-widening-tells.mjs --declaration no --diff PR.DIFF exits 0 but reports 8 of 8 files NOT MEASURED ('no declared surface covers it'). ⚠️ Reported as NOT MEASURED, NOT as a clean reading — it is evidence in neither direction here.", "declared_conflict": "The ruling 5651634638's state paragraph says 'the endpoint carries Clause-②: yes and goes through contract review'. The PM claim 5652065130 declares no, explicitly as a prediction, and names the flip condition (a re-export from plugin-auth/src/index.ts, or a new key on a published payload). Neither holds. Both carriers therefore read no and agree, so no needs:contract-review label was attached. The conflict is stated in the PR body so the seat owning the vocabulary can impose the tier on the ruling's authority if it wants it — that is a card-state act, not this seat's." }, "open_questions": [], "out_of_scope_findings": [ "noted, not filed: ruling row 7 (admit manager_id to the bulk-import tier, resolved in a second pass keyed on email/phone) is ruled but is outside this lane's scope fence. admin-import-users.ts is untouched and its 0-match reading is re-stated in the lint docblock as still current. Successor: the PM, which owns the row-7 split.", "noted, not filed: the ADR-0090 D12 delegated-admin org-chart axis (design 3.3) is NOT declared — ADR-0049 forbids declaring what is not enforced, and a fourth axis on plugin-security/src/delegated-admin-gate.ts is a governed permission-model change. Recorded as the design's own named follow-up. Successor: whoever takes that follow-up; the Entra precedent is attached in the design comment.", "noted, not filed: design 8.1(2) — the dead-slot literal for an unresolved manager reads `manager:undefined`, because `value` is optional and omitted for `manager`. The ruling assigns it to whichever change touches the file first; this PR touches neither approval-service.ts nor approval.zod.ts. Successor: #17931, which is in that file.", "noted, not filed: design 8.1(4) — the #3807 'expanded to nobody' it.each pin table in packages/plugins/plugin-approvals/src/approval-service.test.ts covers team / department / position / org_membership_level and OMITS manager, so the warning for `manager` is real by set membership and not by test; a refactor dropping manager from GRAPH_APPROVER_TYPES would restore the silence this card reports with CI green. Same ruling assignment as above. Successor: #17931.", "noted, not filed: a dedicated error.code per refusal would be the better shape than reusing existing codes plus an error.details.reason discriminator, but StandardErrorCode and ERROR_CODE_LEDGER both live in packages/spec, which this lane is fenced out of. Successor: the domain:spec seat, alongside the ApproverType.describe() item above — the two are one visit to the same package.", "noted, not filed: scripts/tenant-audit-census.mjs emits two [untyped-receiver] errors of its own in packages/plugins/organizations/src/ (claim-org-seed-ownership.ts:93, claim-orphan-org-rows.ts:106) that this diff neither caused nor touches; the CI gate scripts/check-tenant-audit-census.mjs does not act on them and is green. Recorded so a later reader does not attribute them to this PR. Successor: none identified — no open PR or lane is known to this seat that touches those two files.", "boundary, not a finding and deliberately not widening the class: the endpoint's cross-organization and idp_provisioned refusals both FAIL OPEN when the underlying read throws. That is the ruled posture of the routing-time screen (managerIsProvablyOutsideOrg) mirrored on purpose, and the read failure is logged once with its consequence and its remedy rather than degrading silently — pinned by a test." ] }Generated by Claude Code
claude commentedon Sep 13, 2026
Clause-②-correction: 5642248126
Clause-②: no
Session: session_01URLHobLUJB9K1ABV6ofdjj
⛔ This seat's own defect, ⛔ not the delivery's.
check-clause2-carriers --pair 17993answered exit 4: the declaration in this round's claim comment (5652065130) is writtenClause-②: **no**— the value wrapped in markdown bold — so the fixed-spelling predicate does not match it. The reasoning was done and written down; it was put in a spelling the reader does not look for.⭐ This is correction 156 repeating one surface over. 156 recorded that the PR-body reader "is stricter than the card-side one and rejects decorated keys; a decorated key has already cost this lane one red gate." This seat carried that into every dispatch order it wrote for a dev — and then decorated the value in its own claim comments, which are read by the same predicate. ⇒ a rule learned for one carrier was not carried to the other.
⭐ The remedy is also the root fix, and the gate states it: copy the template's literal line rather than composing one. Every miss of this kind is a line composed from memory.
The declaration itself is unchanged and is the measured one:
no, re-derived in-seat from the delivered diff at6149811071— six new exported symbols inadmin-set-user-manager.ts, none reachable from a published entry (src/index.tscarries 0 references and is itself unchanged;package.jsonunchanged, so theexportsmap did not move), with positive controls that discriminate (runAdminBanUser,SYS_USER_PROFILE_EDIT_FIELDSboth present in the rebuiltdist/*.d.ts; all six absent). The endpoint's response is a new payload, ⛔ not a new key on an already-published one.⛔ No second
Claim:— the claim protocol forbids it, and this correction supersedes the declaration in place.Generated by Claude Code
claude commentedon Sep 13, 2026
Clause-②-correction: 5652065130
Clause-②: no
Session: session_01URLHobLUJB9K1ABV6ofdjj
5652627835), which named the wrong comment and was correctly IGNORED.Two of this seat's errors in a row, both worth naming because the second is the more interesting one:
The original defect: this round's claim (
5652065130) wrote the declaration asClause-②: **no**, wrapping the value in markdown bold, so the fixed-spelling predicate does not match it. ⭐ That is correction 156 repeating one surface over — 156 recorded that the reader "rejects decorated keys", this seat carried that into every dispatch order it wrote for a dev, and then decorated the value in its own claim comment, which the same predicate reads.⭐ The repair then failed for a different reason: this seat copied the id out of the gate's remedy TEXT (
5642248126) instead of resolving the remedy's PREDICATE — "the numeric id of the claim comment it corrects", which is this card's governing claim,5652065130. The remedy's number was an illustration; it was read as a value. ⇒ following an instruction's letter while skipping its predicate is its own failure mode, and it is not repaired by re-reading the instruction more carefully — only by resolving what the instruction points AT.✅ The gate refused to fail silently about it, in as many words: "a correction comment WAS posted and did NOT take effect … ⛔ never silently, because a seat that posted it would otherwise wait on a repair that never happened."⚠️ That is exactly what would have happened here.
The declaration itself is unchanged and is the measured one:
no, re-derived in-seat from the delivered diff at6149811071— six new exported symbols inadmin-set-user-manager.ts, none reachable from a published entry (src/index.tscarries 0 references and is itself unchanged;package.jsonunchanged, so theexportsmap did not move), with positive controls that discriminate (runAdminBanUser,SYS_USER_PROFILE_EDIT_FIELDSpresent in the rebuiltdist/*.d.ts; all six absent). The endpoint's response is a new payload, ⛔ not a new key on an already-published one.⛔ No second
Claim:.Generated by Claude Code
claude commentedon Sep 13, 2026
✅ PHASE 3 LANDED —
4f1a56bb0, verified onorigin/mainwith controlsfix(plugin-auth,lint): sys_user.manager_id gains an admin write surface, and themanagerrung stops resolving to nobody (#17993)Probe
(#17993)= 1; positive control(#17988)= 1; negative(#99999)= 0; non-shallow, 13 829 commits. Deliverables printed with substance:origin/mainplugin-auth/src/admin-set-user-manager.ts, 476 linesauth-route-ledger.ts:243—POST /api/v1/auth/admin/set-user-manager,family: 'objectstack-mount', platform-admin gated (ADR-0068)stackWiresManagerChainstill present (4 refs); onlyMANAGER_ONLY_REMEDY/MANAGER_ONLY_ROUTESnow name the endpointadmin-route-nonadmin-refusal.dogfood.test.tsSYS_USER_PROFILE_EDIT_FIELDSunchanged — the endpoint reaches the column by system context, ⛔ not by whitelistWhat the ruling asked for, and what landed
Ruling
5651634638(option B, decision batch #127). Maintainer verbatim: 「同意 经理 = 管理员在用户上显式设置的manager_id;部门负责人 = 单元上的manager_user_id,两者独立」⛔ §4 org-unit derivation stayed CUT —
BusinessUnitGraphService.headOfis not consumed, and the two fields remain independent. ⛔ No delegated-admin axis was declared (ADR-0049 forbids declaring what is not enforced). ⛔ Zeropackages/spec.⭐ Three things this delivery did that a lesser one would not
SYS_USER_PROFILE_EDIT_FIELDSgoes red instead of landing quietly. Most deliveries pin what they changed; this one pinned what it must not.packages/specstop overturned the design's own note. §8.3 recordedApproverType.describe()as "closed in substance" by docs(spec): ApproverType.describe() qualifiesmanagerinstead of offering it as a bare allowed value #17640. Verified rather than assumed —git merge-base --is-ancestor 2eb472418 HEAD→ exit 0, non-shallow — the commit is an ancestor, but what it landed still carries the now-false sentence. ⇒ landed AND stale. A reader trusting that line would have skipped the one edit actually owed. Routed out as spec(approvals):ApproverType.describe()still says the manager column has no product write surface — PR #17993 gives it one, and #17640 left the sentence behind #17995./admin/*mounts readbodybefore the admin check, so a member can get a400identical to an admin's. Measured here: this mount runsgateAdmin(c)as its first statement, so anon →401 UNAUTHENTICATED, member →403 PERMISSION_DENIED. ⇒ the answer is403— but now for a recorded reason, with the divergence written beside it. ⭐ And it caught a vacuity trap unprompted: with no payload entry the handler answers400 INVALID_REQUEST, matching neither a gate refusal nor thediedBeforeTheGatepredicate — "a missing payload would slip through every assertion below as a silent void."Still open, routed rather than dropped
#17995 (
domain:spec) —ApproverType.describe()'s falsified sentence, plus the closed-vocabularyerror.codequestion for the endpoint's five refusals. One visit to one package.manager_idto the bulk-import tier, resolved in a second pass keyed on email/phone) is ruled but not delivered here —admin-import-users.tsis untouched and its 0-match reading still holds. That split is this seat's to carry, ⛔ not lost.In-seat contract review: PASS at
02a036ad3(5652708258, re-issued after the rework moved the head from6149811071). Carrier gate exit 0. Closingcompleted.Generated by Claude Code
fix(plugin-auth,lint): sys_user.manager_id gains an admin write surfa…
feat(plugin-auth): the bulk import admits `manager_id`, resolved in a…
feat(spec): approval onEmptyApprovers gains 'fallback' with a sibling…
feat(platform-objects): declare sys_user set_user_manager row action (#…
feat(platform-objects): declare the sys_user set_user_manager row act…