Skip to content

feat(plugin-auth): the bulk import admits manager_id, resolved in a second pass through the admin write surface's own refusals - #18046

Merged
os-project-manager merged 5 commits into
mainfrom
claude/issue-18028-import-manager-id
Sep 14, 2026
Merged

feat(plugin-auth): the bulk import admits manager_id, resolved in a second pass through the admin write surface's own refusals#18046
os-project-manager merged 5 commits into
mainfrom
claude/issue-18028-import-manager-id

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #18028

Clause-②: yes

Ruling 5651634638 row 7, split out of #16678 by its Phase 3 landing report (5653051072): manager_id is admitted to the bulk-import tier, resolved in a second pass keyed on the importer's identity key, with every row-2 refusal applied per row and an unresolved key reported as a per-row error.

Maintainer verbatim, from the ruling this implements:

同意 经理 = 管理员在用户上显式设置的 manager_id;部门负责人 = 单元上的 manager_user_id,两者独立。

The card's three open questions, answered by measurement

1. Does the importer have a place for a second pass, or does it commit row-by-row? It has one, and no restructure is owed. runAdminImportUsers calls the shared row engine ONCE over the whole batch (runImport, one call) and already carries a post-write section that walks results after it returns — the delivery pass (invitation email / SMS / temporary password) and the run-level audit row both live there, both gated on !prepared.dryRun. createData does commit per row, but nothing observes those commits until runImport has returned, so "every row in the batch exists" is a state the endpoint already reaches on its own. The manager pass is a third phase in that existing section. ⇒ small addition, not a restructure.

2. What does the per-row error channel look like? Two channels, and the card named the right one. rows[] is ImportRowResult (row, ok, action, id, code, error) plus this endpoint's own extensions, of which rows[].delivery is the precedent: a small string union stamped by a post-write phase. The sibling post-write FAILURE (INVITE_EMAIL_FAILED / INVITE_SMS_FAILED) rides code + error on a row that stays created. This change follows both: rows[].manager carries the machine-readable outcome in delivery's shape, rows[].error carries the sentence.

It does not stamp a rows[].code, and that is a fence rather than an oversight — see What is fenced out below.

3. Are N row-wise runSetUserManager calls acceptable, or is a batch seam owed? Row-wise is acceptable at this endpoint's cap; a batch seam is not owed. Per link the derivation issues 1 sys_user read for the user, 1 for the proposed manager, at most 2 sys_member reads, and 1 read per ancestor while walking the chain (1-2 in a flat org, MAX_MANAGER_CHAIN_DEPTH = 20 worst case), then 1 update. Against IMPORT_USERS_MAX_ROWS = 500 that is ~3k reads typically and ~12k worst case — on an endpoint whose module header already records ~100ms/row of scrypt hashing, i.e. ~50s of CPU the batch is already paying. The seam that IS owed is a different one, and it is small:

runSetUserManager(deps, actor, request) takes an HTTP Request, so it could not be driven row-wise as it stood. Rather than fork the predicates, the refusal core is extracted:

export async function applyUserManagerLink(
  deps: SetUserManagerDeps, userId: string, managerId: string | null,
): Promise<SetUserManagerRefusal | null>

runSetUserManager is now exactly that function plus body parsing and an HTTP envelope; the importer is exactly that function called once per row. Zero behaviour change on the endpoint — its own 27-test suite is unchanged and green.

The refusal-reuse fence, and how it is held

⛔ There is no second copy of the five predicates. The importer imports applyUserManagerLink and reports the SetUserManagerRefusal it gets back — status, code and the reason discriminator — without re-wording it. Four of the five are driven end-to-end through the importer in the new suite and asserted to surface with the endpoint's own reason: self_assignment, cycle, idp_provisioned, cross_organization (that last one proves the delegate's sys_member screen really runs, since nothing in the importer reads that table).

The fence is also pinned as an ABSENCE, with a positive control so the absence is a reading rather than a vacuous pass after a rename: the importer's source is asserted to contain applyUserManagerLink and the delegate's module specifier, and to contain zero occurrences of each of the five reason literals, of MAX_MANAGER_CHAIN_DEPTH, and of sys_member.

Proving the pass is really SECOND

A pin that only checks "a manager got resolved" passes against a single-pass resolve that happens to work because the fixture was written in dependency order. The discriminating fixture is deliberately out of order — row 1 names a manager that row 2 creates — and three assertions stand on it:

assertion what it catches
row 1 links to the id row 2 minted the feature
CONTROL: the harness records sys_user at the instant each row was written, and the manager is NOT in the snapshot taken for row 1 proves the forward reference is real, so the pass above is not explained by ordered input
CONTROL: the link's update has a later invocationCallOrder than the LAST createUser a single-pass resolve writes row 1's link before row 2 is created, so this is false for it

Both directions of the per-row error are pinned too, because a test asserting only the first passes against an implementation that aborts everything: the offending row reports (manager: 'unresolved', an error naming the column), AND summary.created is 2, summary.errors is 0, the other row carries no code, and the unlinked user exists with manager_id null — ⛔ not a silent skip.

Reverse verification — three ablations, each with an on-disk proof and a restore proof

Every leg: mutate by exact string replacement, prove the mutation reached disk by occurrence count (never an editor's exit code), run, restore with git checkout HEAD -- path, and prove the restore by blob hash against HEAD plus a clean git diff HEAD. All three legs restored cleanly; the tree ended at git diff HEAD clean.

ablation expected MEASURED
C — withhold the second pass itself (its guard to false) red 14 of 20 red. The 6 that stay green are the ones that should: the empty-cell case, the Tier-1 pin, the source-fence pin, the upsert-patch pin, the dryRun pin, and the fixture-premise control
B — neutralize the DELEGATE's self_assignment predicate red in BOTH suites 2 red, one in each suiteadmin-import-users-manager-pass.test.ts and admin-set-user-manager.test.ts. One mutation, two callers: that is the single derivation, demonstrated rather than asserted
A — withhold the batch-wide in-batch index (idByKey) red GREEN, 20 passed — reported as measured, not as expected. Correct and informative: a created row is in sys_user by the time the pass runs, so the directory lookup resolves it anyway. The index is an OPTIMIZATION (it saves a read per row); the MECHANISM is the pass's position, which is what C and the two ordering controls measure

Control run on the unmutated tree: both suites, 47 passed.

What is fenced out, and routed rather than ridden in

packages/spec is untouched, and one thing wanted to reach into it. The obvious symmetry with INVITE_EMAIL_FAILED would be rows[].code = 'MANAGER_UNRESOLVED' | 'MANAGER_REFUSED'. It was written that way first, and pnpm check:dispatcher-error-vocabulary refused it:

[unclassified-site] packages/plugins/plugin-auth/src/admin-import-users.ts stamps unregistered
code 'MANAGER_REFUSED' (codehelper) and packages/runtime/src/dispatcher-error-vocabulary.ts does
not classify it.
    That file is inside the published face, so the way out is the LEDGER: register
    'MANAGER_REFUSED' in packages/spec/src/api/error-code-ledger.zod.ts under its stamping package

That registration is a packages/spec edit this lane is fenced out of, and #17995 already carries the closed-vocabulary question for this endpoint's refusals. So the code half was removed, not registered and not faked: the machine-readable half lives on rows[].manager (which discriminates unresolved from each refusal reason — finer than two codes would have been) and the sentence on rows[].error. ⛔ Reaching for an already-registered code whose meaning is something else would be the lenient alias Prime Directive #12 refuses. The absence is pinned by a test, so the symmetry cannot be restored without the registration that makes it legal. The gate is green.

Also untouched, deliberately: SYS_USER_PROFILE_EDIT_FIELDS and SYS_USER_IMPORT_UPDATE_FIELDS (asserted unchanged by a test, exactly as #16678's delivery pinned them — the import reaches the column by system context, the way it already reaches phone_number and role); content/docs/releases/**; and §4 org-unit derivation, which the ruling CUT on axis ①.

Clause-② re-derived from the BUILT entry, with both controls

packages/plugins/plugin-auth/src/index.ts:26 carries export * from './admin-import-users.js', so the importer's exports reach the package entry. Measured by occurrence count in dist/index.d.ts after a real build, before and after this change:

symbol before after reading
IMPORT_USERS_MAX_ROWS 2 3 POSITIVE control — a symbol known to be published
runAdminImportUsers 2 2 POSITIVE control
IdentityImportRowResult 0 2 NEW, and in the entry's export { … } clause
ImportManagerOutcome 0 3 NEW, and in the entry's export { … } clause
SetUserManagerRefusalReason 0 3 newly reachable STRUCTURALLY (inlined as ImportManagerOutcome's union), not by name
applyUserManagerLink 0 1 a docblock mention only — ⛔ not an export; admin-set-user-manager.js is NOT in index.ts
SetUserManagerRefusal, runSetUserManager, MAX_MANAGER_CHAIN_DEPTH 0 0 still unreachable from the entry
resolveManagerKey / noteManagerFailure / identityKey / ManagerKeyRef 0 0 NEGATIVE controls — present in the source at 2 / 5 / 9 / 3 occurrences, absent from the built entry

⇒ the card's Clause-②: yes is CONFIRMED, not merely inherited: two named type exports land on the published entry and a third union arrives structurally. The changeset is graded minor accordingly (AGENTS.md: a PR declaring Clause-②: yes takes at least minor), which check-changeset-no-major --base origin/main reads as discharged.

Gates

Head SHA: 2be67d2d4a7bdfc43fe674ab343ea462ba0ac885.

  • pnpm --filter @objectstack/plugin-auth test111 files, 2360 tests, all passed.
  • pnpm --filter @objectstack/plugin-auth typecheckexit 0 (includes check:test-typecheck against tsconfig.test.json).
  • pnpm lint (the whole repo, eslint . --no-inline-config) — exit 0, 2m59s. Not a narrowed run: the full scan.
  • The derived gate families for this diff, via node scripts/pm/dispatch-gates.mjs --commands and reconciled with --ran: 69 derived, 68 run green, 1 NOT MEASURED, 0 unrun, plus 2 run beyond the union.

The one NOT MEASURED family, declared rather than rounded to green:

pnpm check:type-check-debt — exit 3, PREREQUISITE NOT MET. Its first exit 3 named four unbuilt workspace dependencies; the closure was then built (turbo run build --filter='./packages/*' --filter='./packages/*/*', exit 0) and the prerequisite it named is satisfied. It now dies differently: its pinned tsc --max-old-space-size=6144 re-measure is killed by the V8 heap limit on this shared container (FATAL ERROR: Ineffective mark-compacts near heap limit). ⛔ Not a pass and not a finding — nothing was measured. Its sibling pnpm check:type-check-coverage (the non-re-measure half) is exit 0.

⚠️ One result in this run was a false red and is recorded so it is not re-diagnosed: pnpm --filter @objectstack/plugin-auth typecheck reported 25 errors in admin-import-users.ts while the workspace closure build was mid-flight rewriting dist/. Re-run against the finished closure: exit 0.

The full Lint & Repo Gates step sequence is CI's — it is ~200 sequential steps and the derived-family set above is what this seat can measure locally without running the whole farm twice.

Acceptance notes

Noted, not filed — neither is a reproducible defect, a contract violation, or a metadata-authoring trap, so neither meets the filing bar:

⛔ Draft, not flipped, no auto-merge armed — landing is the PM seat's.


Generated by Claude Code


Generated by Claude Code

… second pass

The admin write surface's five refusals are extracted behind one seam,
applyUserManagerLink, and the importer's second pass calls it row-wise
instead of carrying a copy of the predicates.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
…d the refusal fence

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
… spec ledger does not register

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 19 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in applyUserManagerLink; a string literal in runSetUserManager))
  • content/docs/api/error-catalog.mdx (via INVALID_REQUEST (literal, a string literal in runSetUserManager), RESOURCE_NOT_FOUND (literal, a string literal in applyUserManagerLink; a string literal in runSetUserManager))
  • content/docs/api/error-handling-client.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in applyUserManagerLink; a string literal in runSetUserManager))
  • content/docs/api/metadata-api.mdx (via INVALID_REQUEST (literal, a string literal in runSetUserManager))
  • content/docs/automation/approvals.mdx (via manager_id (literal, a string literal in MANAGER_COLUMN))
  • content/docs/automation/webhooks.mdx (via INVALID_REQUEST (literal, a string literal in runSetUserManager), RESOURCE_NOT_FOUND (literal, a string literal in applyUserManagerLink; a string literal in runSetUserManager))
  • content/docs/data-modeling/import-mappings.mdx (via INVALID_REQUEST (literal, a string literal in runSetUserManager))
  • content/docs/kernel/contracts/metadata-service.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in applyUserManagerLink; a string literal in runSetUserManager))
  • content/docs/permissions/authentication.mdx (via temporaryPassword (symbol, a field of interface IdentityImportRowResult), INVALID_REQUEST (literal, a string literal in runSetUserManager))
  • content/docs/permissions/sso.mdx (via INVALID_REQUEST (literal, a string literal in runSetUserManager))
  • content/docs/protocol/kernel/error-handling.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in applyUserManagerLink; a string literal in runSetUserManager))
  • content/docs/protocol/objectql/query-syntax.mdx (via manager_id (literal, a string literal in MANAGER_COLUMN))
  • content/docs/ui/forms.mdx (via INVALID_REQUEST (literal, a string literal in runSetUserManager))

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

  • content/docs/releases/v17/17-0.mdx (via INVALID_REQUEST (literal, a string literal in runSetUserManager), RESOURCE_NOT_FOUND (literal, a string literal in applyUserManagerLink; a string literal in runSetUserManager))

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 anchor(s) matched too much of the corpus to be a work list: sys_user (literal, 35 pages)
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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.

Coarse fallback — 14 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 7ef05f997325c1ca425546bb764fb65eb729d5c6packageMentionDocs.

Which tree this was computed on

This run read content/docs from aa9b95be91c1669e8e24e65d0bc473dc6ea91075 — the merge of head 6ff3e0b783a78b35ed3a02d53d5eebc344007440 into base 7ef05f997325c1ca425546bb764fb65eb729d5c6, 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 aa9b95be91c1669e8e24e65d0bc473dc6ea91075 && git checkout aa9b95be91c1669e8e24e65d0bc473dc6ea91075
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7ef05f997325c1ca425546bb764fb65eb729d5c6 6ff3e0b783a78b35ed3a02d53d5eebc344007440 && git checkout -B drift-repro 7ef05f997325c1ca425546bb764fb65eb729d5c6 && git merge --no-ff 6ff3e0b783a78b35ed3a02d53d5eebc344007440

node scripts/docs-audit/affected-docs.mjs --json 7ef05f997325c1ca425546bb764fb65eb729d5c6

⚠️ 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 7ef05f997325c1ca425546bb764fb65eb729d5c6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

🔴 Serial-relay hold — this PR and a sibling both append to scripts/engine-double-contract.pinned.json

#18022 (card #17516) and #18046 (card #18028) each add one row to that generated ledger, from the same baseline. Two independent +1s into a sorted JSON list merge cleanly — exit 0, no conflict marker — and one side can be dropped with nothing erroring.

Counts taken on every side before any merge, which is the whole point: once the first of the two lands, 1492 on main looks entirely normal and the loss is invisible.

side rows
origin/main c185d087b 1491
#18022 merge-base 32a321430 1491
#18046 merge-base 1e20f816e 1491
#18022 head 498f60543 1492
#18046 head 2be67d2d4 1492

the only correct end state once both have landed is 1493. A reading of 1492 means a side was silently swallowed.

⛔ One baton at a time. Whichever of the two lands second must, before enqueueing: merge main, regenerate with the repo's own --write, and prove 0 lost AND byte-identical, with the row count reading 1493. ⛔ The merge exiting 0 is not evidence. ⛔ Path-disjointness is not evidence either — that reasoning was wrong and is recorded as such (correction 170, amended against correction 149: the hazard is the ordinary text merge, and this file carries no merge=os-regen driver, so checking the driver and finding none proves nothing).

Re-measure with:

git show <ref>:scripts/engine-double-contract.pinned.json | python3 -c \
  "import json,sys;d=json.load(sys.stdin);print(sum(len(v) for v in d.values()) if isinstance(d,dict) else len(d))"

Recorded in the seat registry (#6021 §3). Neither PR is enqueued today in any case — both wait on #18032.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Head reviewed: 2be67d2d4a7bdfc43fe674ab343ea462ba0ac885
Implemented-by: claude/issue-18028-import-manager-id (mode:subagent — the branch, not a session)
Reviewed-by: session_01URLHobLUJB9K1ABV6ofdjj (domain:services execution seat)

① Clause-② — yes, and the BASIS is not the one the diff's shape suggests

The declaration is right. The reason is worth pinning down, because a reader skimming the diff would pin it to the wrong symbols.

plugin-auth/src/index.ts carries 29 export * lines. ./admin-import-users.js is among them (:26); ./admin-set-user-manager.js is NOT, and no re-exported module re-exports it (scanned all 29). ⚠️ export * carries what a module exports, ⛔ never what it imports — and admin-import-users.ts:117 imports the seam (} from './admin-set-user-manager.js') with no export … from anywhere.

applyUserManagerLink, SetUserManagerRefusal and SetUserManagerRefusalReason are NOT reachable from the published entry. ⛔ The yes does not rest on them.

It rests on admin-import-users.ts, which is published:

limb reading
(a) new exported symbol reachable from the entry export type ImportManagerOutcome (:312)
(b) new key on an already-published payload rows[].manager?: ImportManagerOutcome (:329) and summary.manager: { linked, unresolved, refused }

⇒ changeset minor ✅, needs:contract-review on both carriers ✅.

🔴 Finding — a published type names an UNPUBLISHED one

// admin-import-users.ts:312  (reachable from the entry)
export type ImportManagerOutcome = 'linked' | 'unresolved' | SetUserManagerRefusalReason;
//                                                           ^ not reachable from the entry

A consumer can receive the value and switch on the literal strings, but ⛔ cannot import type { SetUserManagerRefusalReason } from the package entry to name the arm, and cannot see its members from the entry's surface. It compiles — both modules live in the same dist — so no gate catches it.

⚠️ Stated as a finding for the delivery to answer, ⛔ not a defect I assert: it may be deliberate (keep the seam internal). The question is one line — is ImportManagerOutcome meant to be nameable end-to-end by a consumer? If yes, the entry should re-export the reason type; if no, say so and the union arm should be spelled out inline instead of borrowing a private name. The predicate to settle it: whether the built dist/index.d.ts carries the literal members or a bare reference to the unexported name.

⚠️ It may also be moot shortly — #17995 item 2 is already queued to decide dedicated error.codes for these same five refusals, which would reshape this type.

② The fence this card exists for — HELD, and verified independently

The card's central instruction was ⛔ do not re-derive the five refusals inside the importer. Measured on the head, ⛔ not read off the report:

reading admin-import-users.ts
applyUserManagerLink 4
SetUserManagerRefusalReason 3
idp_provisioned · self_assignment · max_depth_exceeded · cross_organization 0 each

⭐ And the seam was extracted rather than the predicates copiedrunSetUserManager is now applyUserManagerLink (the decision) plus body parsing and an HTTP envelope, with the reason written into the source:

"A refusal BEFORE it is dressed as an HTTP envelope … so a caller that is not an HTTP request — the bulk importer's second pass — routes the very same predicates onto its own per-row channel. ⛔ A second copy of the five refusals inside the importer is the drift #15706 already cost this platform once; there is ONE derivation and this is it."

⭐ The ablation proves the single derivation instead of asserting it: neutralising the delegate's self_assignment predicate turns one test red in each suite — importer and endpoint — from one mutation.

packages/spec — the red line held, and it held the EXPENSIVE way

A row-level error.code (MANAGER_UNRESOLVED / MANAGER_REFUSED) was written first and check:dispatcher-error-vocabulary refused it as unregistered. ⛔ The code half was removed rather than registered in the fenced packages/spec ledger, and ⛔ rather than faked with a mismatched registered code — the cheap exit that would have shipped a lie. The absence is pinned by a test, and the successor is named (#17995 item 2, same file, one visit).

packages/spec hits: 0. content/docs/releases/**: 0. Both measured from GET /pulls/18046/files.

④ The second pass is proved by a DISCRIMINATING case, ⛔ not by a happy path

The fixture is deliberately out of dependency order — row 1 names a manager that row 2 creates — with two controls a single-pass implementation cannot satisfy: the harness snapshots sys_user at each createUser and the manager is absent from row 1's snapshot, and the link's update invocationCallOrder is asserted greater than the last createUser's.

Per-row error pinned both directions: the offending row reports AND summary.created is 2 with summary.errors 0, the other row carries no code, and the unlinked user exists with manager_id null. ⛔ A one-directional pin would pass against an implementation that aborts the whole import.

⭐ Ablation leg A came back GREEN and was reported as MEASURED rather than as expected — the in-batch index is an optimisation; the mechanism is the pass's position, which legs C and the two ordering controls measure. Reporting a negative ablation honestly is worth more than the leg itself.

⑤ Gates — one NOT MEASURED, correctly refused as neither pass nor finding

69 derived families, 68 green, 0 unrun, 1 NOT MEASURED: check:type-check-debt exited 3 (PREREQUISITE NOT MET); the closure it named was built (turbo run build, exit 0), after which it dies differently — its own pinned tsc --max-old-space-size=6144 is OOM-killed on this container, reproduced 3×. ⛔ Not recorded as a pass. Its sibling check:type-check-coverage is exit 0. Whole-repo eslint . --no-inline-config exit 0 — a full scan, ⛔ not a narrowed one.

⭐ A false red was recorded so it is not re-diagnosed: plugin-auth typecheck reported 25 errors in admin-import-users.ts while the closure build was mid-flight rewriting dist/; re-run against the finished closure it is exit 0.

Verdict: PASS WITH ONE FINDING at 2be67d2d4

The finding in ① is a published-surface nameability question, ⛔ not a fence breach — every fence this card set held, and the one that mattered held by extraction rather than by copying.

⚠️ Binds to the head it names. Landing is held on three separate things, none of them this PR's quality:

  1. Queue-flake anchor: test/format-zod-union.test.ts #18032 — the domain:cli format-zod-union defect ejects every PR that reaches the queue.
  2. Serial relay — this PR and fix(plugin-security): a permission-set name collision now reaches the author #18022 each add one row to scripts/engine-double-contract.pinned.json (both heads 1492, main 1491). One baton at a time; the second to land must regenerate and read 1493. See comment 5654081472.
  3. CICI and Lint & Type Check are still in progress at this writing, so pre-check ③ is NOT MEASURED, ⛔ not a pass.

Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

🟢 Baton taken — #18022 is the FIRST of the two to go; #18046 holds

The blocker cleared: #18043 merged at 15:23:53Z (fb2f01dde), verified by content probe on origin/main with a positive ((#17993) = 1) and negative ((#99999) = 0) control.

Re-measured on origin/main 226970bbe just now, ⛔ not carried forward from the earlier reading:

side rows
origin/main 1491
#18022 head 498f60543 1492
#18046 head 2be67d2d4 1492
commits on main touching the ledger since either merge-base 0

⇒ the arithmetic is unchanged: the first lander takes main to 1492, and the second must reach 1493.

#18022 is armed (ready-flipped via MCP with a draft:false read-back, auto-merge armed 15:32:30Z). #18046 stays held until #18022 is MERGED and main reads 1492 — then #18046 merges main, regenerates with the repo's own --write, and must show 0 lost, byte-identical, and 1493 before it is enqueued.

⚠️ Why #18022 went first, stated so it is not read as arbitrary: its review is an unconditional PASS, #18046's carries an open finding (the published type naming an unpublished one), and its card is the older of the two. ⛔ Nothing about delivery quality — both are green work.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review — independent clause-② record at 2be67d2d4a7bdfc43fe674ab343ea462ba0ac885

Served-tier: claude-fable-5-1
Implemented-by: claude/issue-18028-import-manager-id
Reviewed-by: session_01SausTaCtH292F1JAAtCCS7

Head reviewed: 2be67d2d4a7bdfc43fe674ab343ea462ba0ac885 (merge-base 1e20f816ea81f2ba02e9ab2dd6a60bd183150e0a). Commissioned by the domain:services seat (session_01URLHobLUJB9K1ABV6ofdjj) as an independent review because that seat reports being served claude-opus-5, below CONTRACT_REVIEW_TIER; its own review (5654105036) was read as input, ⛔ not inherited — every reading below was re-derived on a detached worktree at the head, outside any repo checkout.

Step 0 — served tier, read from get_session (session_id omitted), verbatim: session_context.model = claude-fable-5-1 · external_metadata.last_served_model = claude-fable-5-1 · configured_model = claude-fable-5-1. CONTRACT_REVIEW_TIER read from scripts/pm/dispatch-gates.mjs:10510 = claude-fable-5-1. Equal ⇒ this record is rendered at tier.

Verdict: PASS

Clause-②: yes is the correct declaration — both limbs of the mechanical floor are hit — and the changeset grade (@objectstack/plugin-auth: minor) matches it. Nothing in the diff widens the surface beyond what the declaration and the grade already admit.

① Symbol-by-symbol reachability from the published entry

The published entry is package.json exports["."].typesdist/index.d.ts, built by tsup from src/index.ts (files = dist, README.md, CHANGELOG.md). The only other entry, ./rate-limit-storage, imports one type from @better-auth/core and nothing from src/, so it cannot carry any of these. src/index.ts is byte-identical between merge-base and head (git diff --quiet exit 0); it carries 29 export * lines, ./admin-import-users.js at :26, and ./admin-set-user-manager.js on none of them — at head AND at merge-base.

symbol declared in new in this PR re-export path to src/index.ts in built dist/index.d.ts export { … } clause clause-② reading
ImportManagerOutcome (type) admin-import-users.ts:312 yes index.ts:26 export * yestype ImportManagerOutcome in the clause; declared at dist :4319 (3 occurrences total) limb (1) HIT — new exported symbol, reachable
IdentityImportRowResult (interface) admin-import-users.ts:320 yes (replaces an inline anonymous type on the results array) index.ts:26 export * yestype IdentityImportRowResult in the clause (2 occurrences) limb (1) HIT — new exported symbol, reachable
SetUserManagerRefusal (interface) admin-set-user-manager.ts yes none — 0 export … from './admin-set-user-manager' in src/ no — 0 occurrences anywhere in dist/index.d.ts not reachable by name; no limb
applyUserManagerLink (function) admin-set-user-manager.ts yes none no — 1 occurrence, a docblock sentence at dist :4248; 0 in the clause not reachable; no limb
SetUserManagerRefusalReason (type) admin-set-user-manager.ts:134 (pre-existing since #16678) no none no — 3 occurrences: an unexported local type SetUserManagerRefusalReason = … at dist :4164 with all nine members inlined, the reference inside ImportManagerOutcome at :4319, and one {@link}; 0 in the clause not reachable by name; its members arrive structurally through ImportManagerOutcome — see ④
runSetUserManager, MAX_MANAGER_CHAIN_DEPTH admin-set-user-manager.ts (pre-existing) no none (only consumer is a dynamic import() in auth-plugin.ts:2359, a value import, not a re-export — same at merge-base) no — 0 occurrences each unchanged; no limb
resolveManagerKey, noteManagerFailure, identityKey, ManagerKeyRef admin-import-users.ts (module-private) yes not exported from their module 0 / 1 (a {@link noteManagerFailure} in a docblock at dist :4257) / 0 / 0; none in the clause NEGATIVE controls
IMPORT_USERS_MAX_ROWS, runAdminImportUsers admin-import-users.ts (pre-existing) no index.ts:26 yes — both in the clause (3 and 2 occurrences) POSITIVE controls — known-published symbols

Built-entry control — dist/index.d.ts at merge-base vs head, each from a real turbo run build --filter='@objectstack/plugin-auth...' (27/27 tasks, exit 0) in its own detached worktree. occ = whole-word occurrences in the file; clause = occurrences inside the final export { … } line (base line 6986, head line 7117).

symbol base occ head occ base clause head clause
ImportManagerOutcome 0 3 0 1
IdentityImportRowResult 0 2 0 1
SetUserManagerRefusal 0 0 0 0
applyUserManagerLink 0 1 (docblock) 0 0
SetUserManagerRefusalReason 0 3 (local unexported type, one reference, one {@link}) 0 0
runSetUserManager · MAX_MANAGER_CHAIN_DEPTH 0 0 0 0
resolveManagerKey · identityKey · ManagerKeyRef (NEGATIVE) 0 0 0 0
noteManagerFailure (NEGATIVE) 0 1 ({@link} in a docblock) 0 0
IMPORT_USERS_MAX_ROWS (POSITIVE) 2 3 1 1
runAdminImportUsers (POSITIVE) 2 2 1 1

diff of the two export { … } lines, split on commas: +type IdentityImportRowResult, +type ImportManagerOutcome, nothing removed. That is the whole named widening of the published entry, and it is exactly what the declaration and the minor grade admit.

Limb (2) — new keys on an already-published payload. The 200 body of POST /api/v1/auth/admin/import-users at merge-base is data.summary{total, created, updated, skipped, errors, dryRun, passwordPolicy, delivery, mode, matchBy} and data.rows[] typed ImportRowResult & { temporaryPassword?, delivery? }. At head, data.summary gains manager: { linked, unresolved, refused } and data.rows[] gains manager?: ImportManagerOutcome. Two new keys on a published payload ⇒ limb (2) HIT. ⛔ No rows[].code is added — the diff removes the MANAGER_UNRESOLVED / MANAGER_REFUSED stamping that check:dispatcher-error-vocabulary refused, and the test pins the absence; no new key lands on the error-code vocabulary.

The remaining two files carry no surface: .changeset/18028-import-users-manager-second-pass.md (the grade) and scripts/engine-double-contract.pinned.json (+1 ledger row naming the new test file; not a published package).

② Semver grade

minor on @objectstack/plugin-auth. AGENTS.md: a Clause-②: yes PR takes at least minor. Every change is additive (two new named types, two new optional/new keys, no removal, no rename, no accept-set narrowing — SYS_USER_PROFILE_EDIT_FIELDS / SYS_USER_IMPORT_UPDATE_FIELDS untouched and pinned), so major is not owed. Consistent.

③ Boundary flags

os-dev-report on #18028 carries open_questions: [] and three out_of_scope_findings, each with a named successor: the row-level error.code registration → #17995 item 2 (same file, same five refusals); the parent's patch-graded .changeset/16678-admin-set-user-manager.md → the release seat at version time (this PR's minor on the same package already governs the next release); the platform-appended second footer → left as-is per AGENTS.md. No flag is unanswered; none needs escalation.

④ Independent judgement on the commissioning seat's finding — "a published type names an unpublished one"

Re-derived, and it is factually right: ImportManagerOutcome is reachable from the entry; the SetUserManagerRefusalReason arm it references is not reachable by name (nor is anything else from admin-set-user-manager.ts). Measured, not inherited: 0 export … from './admin-set-user-manager' in src/ (positive control: the same regex finds index.ts:26 for ./admin-import-users), 0 consumers anywhere in the repo naming SetUserManagerRefusalReason / ImportManagerOutcome / IdentityImportRowResult / applyUserManagerLink outside the two modules and their tests.

What it means for clause-②, decided explicitly:

  • Clause-② speaks to the exported alias, not to the type it references. The floor is "a new exported symbol reachable from the entry" — ImportManagerOutcome is one, and the yes stands on it (plus IdentityImportRowResult and the two payload keys) whether or not its arm is nameable. The finding neither adds a hit nor removes one: the verdict and the grade are the same with the arm exported or not.

  • The PR does not change the manager module's entry status. admin-set-user-manager.ts was landed by { type: 'manager' } resolves a column no product surface can write: sys_user.manager_id is refused by the data API and absent from the auth admin endpoints #16678 with index.ts not re-exporting it, and this PR leaves index.ts byte-identical. Whether that seam module should be on the published face is the parent card's decision (and 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 item 2 is about to reshape the vocabulary), not a regression this PR introduces. Its NEW exports (applyUserManagerLink, SetUserManagerRefusal) inherit the same non-published status, so they are not clause-② hits either — which is the reading a "the word export appears in the diff" test would have got wrong.

  • A consumer is not blinded. The built dist/index.d.ts does not leave a dangling reference: tsup inlines type SetUserManagerRefusalReason = 'invalid_body' | 'user_not_found' | 'manager_not_found' | 'self_assignment' | 'cycle' | 'max_depth_exceeded' | 'cross_organization' | 'idp_provisioned' | 'engine_unavailable' at :4164 as a local, unexported declaration, so every member of ImportManagerOutcome is visible and narrowable from the entry — only the NAME of the arm is not importable. The arm is nameable today as Exclude<ImportManagerOutcome, 'linked' | 'unresolved'> from the entry alone.

  • Not a defect under the filing bar (Prime Directive chore: version packages #10: repro, violated contract text, or authoring trap — none applies), and not a fence breach of this card. It is a nameability note. If the seat wants the reason type nameable end-to-end, the fix is one line in src/index.tsexport type { SetUserManagerRefusalReason } from './admin-set-user-manager.js' — and that line is itself a clause-② widening that would need its own declaration; ⛔ it is not owed for this landing.

  • One adjacent nit the seat did not raise, measured here and stated as a NOTE, not a finding. SetUserManagerRefusalReason has nine members at merge-base and at head (unchanged by this PR), not the five the card lists: the four extra are the endpoint's own invalid_body, user_not_found, manager_not_found, engine_unavailable. Three of those can genuinely reach a row through applyUserManagerLink (the engine gone, a user deleted mid-batch, a manager id that vanished between resolve and link), so admitting them on rows[].manager is right. invalid_body cannot — the body-parse refusals stay in runSetUserManager before the seam is called — so the published type over-claims by exactly one member a consumer will never receive. Nothing is widened and no accept-set changes; if the delivery wants the type exact, Exclude<SetUserManagerRefusalReason, 'invalid_body'> on the alias is the one-token fix, and 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 item 2 is about to revisit this vocabulary anyway. ⛔ Not blocking.

⇒ The finding does not turn the verdict. PASS stands.

Commands run (all on the detached worktree at 2be67d2d4, sibling of the repo checkout, never inside it)

git worktree add --detach ../objectstack-review-18046 2be67d2d4a7bdfc43fe674ab343ea462ba0ac885
git merge-base HEAD origin/main                                   # 1e20f816e
git diff --quiet 1e20f816e HEAD -- packages/plugins/plugin-auth/src/index.ts   # exit 0 (identical)
git diff 1e20f816e HEAD -- …/admin-import-users.ts …/admin-set-user-manager.ts | grep -E '^[+-].*\bexport\b'
grep -rnE "^\s*export\s+(\*|\{[^}]*\}|type\s+\{[^}]*\})\s+from\s+['\"]\./admin-set-user-manager(\.js)?['\"]" src/   # 0
grep -rnE "…/admin-import-users(\.js)?['\"]" src/                # index.ts:26 (positive control)
git grep -n admin-set-user-manager HEAD 1e20f816e -- 'packages/plugins/plugin-auth/src/*.ts'
python3 -c '…package.json exports/files…'
node scripts/pm/check-widening-tells.mjs --declaration no  --diff pr18046.diff   # 5 files NOT MEASURED — no declared surface covers plugin-auth; the gate saw nothing
node scripts/pm/check-widening-tells.mjs --declaration yes --diff pr18046.diff   # control: never blocks a yes
pnpm install --frozen-lockfile && turbo run build --filter='@objectstack/plugin-auth...'   # head, real dts build
turbo run build --filter='@objectstack/plugin-auth...'   # merge-base worktree (../objectstack-review-18046-base), same deps
for s in …; do grep -ow "$s" dist/index.d.ts | wc -l; done   # both trees; plus the same count restricted to the final `export {` line
diff <(base export-clause names) <(head export-clause names)   # +2 / -0

Not measured, stated plainly

  • node scripts/pm/check-clause2-carriers.mjs --pair 18046NOT MEASURED here; it is the landing seat's pre-check and reads this very comment, so it is meaningful only after this record is posted.
  • CI on the head, the serial-relay row count on scripts/engine-double-contract.pinned.json (1492 vs the 1493 owed after fix(plugin-security): a permission-set name collision now reaches the author #18022), and the merge of mainNOT MEASURED and out of this record's scope; the seat owns them.
  • pnpm --filter @objectstack/plugin-auth testNOT MEASURED in this session; the reviewed head's own tests are the seat's ③ pre-check, and this record judges clause-② only.

⛔ Labels, draft state, auto-merge and the branch were not touched.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Served-tier: claude-fable-5-1
Implemented-by: claude/issue-18028-import-manager-id
Reviewed-by: session_016X5iS7xCzauPZfMwh4VtcT

Contract review — independent clause-② record at 6ff3e0b783a78b35ed3a02d53d5eebc344007440

Head reviewed: 6ff3e0b783a78b35ed3a02d53d5eebc344007440 (merge commit; parents 2be67d2d4a7bdfc43fe674ab343ea462ba0ac885 and c54d8d67b831fe106a1f02a9fdcd88764ff81592 = the PR's base). Merge-base with origin/main: c54d8d67b. Card #18028. Commissioned by the domain:services seat (session_01URLHobLUJB9K1ABV6ofdjj) because that seat is served below CONTRACT_REVIEW_TIER. The prior at-tier record (5656683558, at 2be67d2d4) was read as a hypothesis, ⛔ not inherited — every reading below was re-derived on this head in this session.

Step 0 — served tier, get_session with session_id omitted, verbatim: session_context.model = claude-fable-5-1 · external_metadata.last_served_model = claude-fable-5-1 · configured_model = claude-fable-5-1. CONTRACT_REVIEW_TIER at scripts/pm/dispatch-gates.mjs:10510 = claude-fable-5-1. Equal ⇒ rendered at tier.

Verdict: PASS

Clause-②: yes is the correct declaration on this head — both limbs are hit — and the changeset grade (.changeset/18028-import-users-manager-second-pass.md: '@objectstack/plugin-auth': minor) matches it. Nothing in the diff widens the published entry beyond what the declaration and the grade admit.

Is the clause-② surface unchanged from 2be67d2d4? MEASURED — yes

The sync lap is a single merge commit. Blob hashes on the four files that decide reachability are identical between 2be67d2d4 and 6ff3e0b78:

file blob at 2be67d2d4 blob at 6ff3e0b78
packages/plugins/plugin-auth/src/admin-import-users.ts 0488d3b1 0488d3b1 — SAME
packages/plugins/plugin-auth/src/admin-set-user-manager.ts 513ad5d0 513ad5d0 — SAME
packages/plugins/plugin-auth/src/index.ts e1612f1b e1612f1b — SAME
packages/plugins/plugin-auth/package.json cea37b9e cea37b9e — SAME

git diff --stat 2be67d2d4 6ff3e0b78 -- <the PR's 5 files> touches exactly one: scripts/engine-double-contract.pinned.json, +5 lines (one ledger row naming plugin-security/src/permission-set-name-collision.test.ts, which main brought in). Row counts: origin/main 1492 · 2be67d2d4 1492 · 6ff3e0b78 1493. That file is not a published package and carries no clause-② surface. ⇒ The surface is the same as the prior record's, and this record now binds it to 6ff3e0b78.

① Symbol-by-symbol reachability from the published entry

The published entry is package.json exports["."].typesdist/index.d.ts, built by tsup from src/index.ts (files = dist, README.md, CHANGELOG.md; package.json unchanged by the PR). The only other entry, ./rate-limit-storage, imports one type from @better-auth/core and nothing from src/; its built .d.ts has 0 hits for any diff identifier. src/index.ts is unchanged by the PR: it carries export * from './admin-import-users.js' at :26 and no export … from './admin-set-user-manager.js' on any line. The full-src/ scan for ^export (*|{…}|type {…}) from finds no module re-exporting admin-set-user-manager (the only non-test references are the type/value import at admin-import-users.ts:117 and a dynamic value import() at auth-plugin.ts:2359 — neither is a re-export; export * carries what a module exports, ⛔ never what it imports).

Built-entry probe on this head: pnpm install --frozen-lockfile then turbo run build --filter='@objectstack/plugin-auth...' (27/27 tasks, exit 0). file = whole-word occurrences in dist/index.d.ts (7117 lines); clause = occurrences inside the final export { … } line (line 7117).

symbol declared in new in PR path to src/index.ts file clause clause-② reading
ImportManagerOutcome (type) admin-import-users.ts:312 yes index.ts:26 3 1 limb (1) HIT — new exported symbol, reachable
IdentityImportRowResult (interface) admin-import-users.ts:320 yes (base had an inline anonymous type on results) index.ts:26 2 1 limb (1) HIT — new exported symbol, reachable
SetUserManagerRefusal (interface) admin-set-user-manager.ts:191 yes none 0 0 not reachable; no limb
applyUserManagerLink (function) admin-set-user-manager.ts:418 yes none 1 (docblock at dist :4248) 0 not reachable; no limb
SetUserManagerRefusalReason (type) admin-set-user-manager.ts:134 (pre-existing) no none 3 (local unexported type at dist :4164 with all 9 members inlined; the reference at :4319; one {@link}) 0 not reachable by name; members arrive structurally — see ②
SetUserManagerDeps, runSetUserManager, MAX_MANAGER_CHAIN_DEPTH admin-set-user-manager.ts (pre-existing) no none 0 / 0 / 0 0 unchanged; no limb
resolveManagerKey, noteManagerFailure, identityKey, ManagerKeyRef admin-import-users.ts (module-private) yes not exported from their module 0 / 1 ({@link}) / 0 / 0 0 NEGATIVE controls
IMPORT_USERS_MAX_ROWS, runAdminImportUsers, IdentityImportEngine admin-import-users.ts (pre-existing) no index.ts:26 3 / 2 / 3 1 / 1 / 1 POSITIVE controls — known-published symbols, seen by the probe

Limb (2) — new keys on an already-published payload (measured in source, since the 200 body rides the generic EndpointResult { status, body } and its keys are not named in the .d.ts): at merge-base admin-import-users.ts contains the word manager 0 times. At head, data.rows[] gains manager?: ImportManagerOutcome (:329, and visible in dist :4335) and data.summary gains manager: managerLinks = { linked, unresolved, refused } (:861 dry-run branch, :913 write branch). ⇒ limb (2) HIT. ⛔ No rows[].code for the manager outcome: MANAGER_UNRESOLVED / MANAGER_REFUSED occur only in one docblock (:342), never as a stamped value.

② The ImportManagerOutcomeSetUserManagerRefusalReason question, decided explicitly

Re-derived on this head: index.ts re-exports ./admin-import-users.js and not ./admin-set-user-manager.js; nothing else re-exports it. So the alias is reachable and the type it references is not reachable by name. What that means for clause ②:

  • Clause ② speaks to the exported alias, not to the type it references. ImportManagerOutcome is a new exported symbol reachable from the entry, so limb (1) is hit by it regardless of whether its arm is nameable. SetUserManagerRefusalReason is neither new nor reachable, so it contributes no hit on its own. The verdict and the grade are identical whether or not that arm were re-exported.
  • A consumer of the entry is not left with a dangling reference. tsup inlined the arm as a local unexported type SetUserManagerRefusalReason = 'invalid_body' | … | 'engine_unavailable' at dist :4164, so every member of ImportManagerOutcome is visible and narrowable from the entry; only the arm's name is not importable (it remains nameable as Exclude<ImportManagerOutcome, 'linked' | 'unresolved'>).
  • The PR does not change the manager module's entry status. admin-set-user-manager.ts was off the published face before this PR and index.ts is byte-identical; its two new exports inherit that status and are not clause-② hits. Re-exporting the reason type would itself be a further widening owed its own declaration; ⛔ not owed for this landing. A nameability note, not a defect and not a fence breach.

Commands run (repo clone at /home/user/objectstack, PR head checked out; ⛔ no worktree)

mcp: get_session (session_id omitted)                              # Step 0, verbatim above
git fetch origin pull/18046/head:pr18046 && git checkout pr18046   # 6ff3e0b783a78b35ed3a02d53d5eebc344007440
git merge-base pr18046 origin/main                                 # c54d8d67b
git rev-list --parents -n1 pr18046                                 # 6ff3e0b78 2be67d2d4 c54d8d67b
git diff --stat c54d8d67b pr18046                                  # 5 files, +994 −22
git diff c54d8d67b pr18046 -- …/admin-import-users.ts …/admin-set-user-manager.ts | grep -E '^[+-].*\bexport\b'
git diff --stat c54d8d67b pr18046 -- …/src/index.ts …/package.json   # empty (unchanged)
for f in <4 files>; do git rev-parse 2be67d2d4:$f; git rev-parse pr18046:$f; done   # SAME-BLOB ×4
git diff --stat 2be67d2d4 pr18046 -- <the PR's 5 files>            # only pinned.json, +5
git show <ref>:scripts/engine-double-contract.pinned.json | python3 -c '…row count…'   # 1492 / 1492 / 1493
grep -rnE "from ['\"]\./admin-set-user-manager(\.js)?['\"]" src   # only admin-import-users.ts:117 (an import)
grep -rnE "^export (\*|\{[^}]*\}|type \{[^}]*\}) from" src        # no re-export of admin-set-user-manager
grep -rnE "admin-set-user-manager" src --include=*.ts             # + auth-plugin.ts:2359 dynamic import()
git show c54d8d67b:…/admin-import-users.ts | grep -cE '\bmanager\b'   # 0
node -e '…package.json exports/files…'
pnpm install --frozen-lockfile                                     # exit 0
pnpm turbo run build --filter='@objectstack/plugin-auth...'        # 27/27, exit 0
for s in …; do grep -ow "$s" dist/index.d.ts | wc -l; sed -n '7117p' dist/index.d.ts | grep -ow "$s" | wc -l; done
grep -nwE SetUserManagerRefusalReason dist/index.d.ts              # :4164 local type, :4315 link, :4319 reference
grep -cE 'ImportManagerOutcome|IdentityImportRowResult|SetUserManager|applyUserManagerLink' dist/rate-limit-storage.d.ts   # 0
cat .changeset/18028-import-users-manager-second-pass.md           # '@objectstack/plugin-auth': minor

Not measured, stated plainly

  • A merge-base build of dist/index.d.ts for a before/after diff of the export { … } clause — NOT MEASURED in this session. The head-only probe carries its own positive controls (three known-published symbols seen in the clause) and negative controls (four module-private identifiers absent from it), and the base-side absence of the new names is established at source level (manager 0 at base; index.ts unchanged), which is sufficient for a positive verdict.
  • Tests, typecheck, lint, CI on the head, the serial-relay regeneration proof (--write, 0 lost, byte-identical) — NOT MEASURED; out of this record's scope and the seat's.
  • node scripts/pm/check-clause2-carriers.mjs --pair 18046NOT MEASURED; it reads this comment and is meaningful only after it is posted.

⛔ Labels, draft state, auto-merge and the branch were not touched.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Carrier cleared on both carriers — provenance

Record cited: 5657354956Verdict: PASS, rendered at CONTRACT_REVIEW_TIER by session_016X5iS7xCzauPZfMwh4VtcT, naming head 6ff3e0b783a78b35ed3a02d53d5eebc344007440 — this PR's current head.

Served-tier: claude-fable-5-1
Implemented-by: claude/issue-18028-import-manager-id
Reviewed-by: session_016X5iS7xCzauPZfMwh4VtcT

⭐ That record replaces 5656683558, which named the pre-relay head 2be67d2d4 and stopped governing when the sync lap moved the head. A record binds to the head it names. ⚠️ And the replacement did not merely re-affirm the old one — it proved the clause-② surface was carried across unchanged, by comparing blob hashes on the four files that decide reachability rather than assuming a sync lap is inert.

Commissioned under the maintainer's ruling of 2026-09-13T22:2xZ, 「你可以直接派fable复核」.

carrier before after
card #18028 domain:services, needs:contract-review, pm:dispatched, priority:p2 domain:services, pm:dispatched, priority:p2
PR #18046 documentation, needs:contract-review, size/xl, tests, tooling documentation, size/xl, tests, tooling

The gate, run on BOTH sides of the strip

scripts/pm/check-clause2-carriers.mjs --pair 18046, exit captured before any pipe, from a worktree at origin/main:

when exit
before the strip 0
after the strip — the landing gate 0"and a review of record names this head"

Landing state

  • 11 workflow runs, 11 distinct names (= the open-PR reference), 0 non-green. PR Automation success.
  • check-governed-merges.mjs --pr 180460 of 5 paths hit the register ⇒ NOT governed.
  • Clause-②: yes carries the level rule, and the changeset grades @objectstack/plugin-auth: minor — the only package this diff moves under packages/**/src/**. ⇒ satisfied, ⛔ not dodged by regrading.

⭐ Serial relay — second baton, proven

scripts/engine-double-contract.pinned.json at this head reads 1493, with #18022's row (1 hit), this PR's row (1 hit), and a negative control (0). The lap's report also carried an ablation: it deleted #18022's row, proved the mutation reached disk (hash change, grep 1→0, count 1493→1492 — the exact swallow signature), then showed --write reported 1 added or grown, 0 lost and restored byte-identically. ⇒ the regenerator is demonstrably not a no-op, so the zero-delta regeneration means something.

main currently reads 1492; on landing it must read 1493.


Generated by Claude Code

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 size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ruling row 7] the user bulk import must admit manager_id, resolved in a second pass keyed on the importer's identity key — split from #16678

2 participants