Skip to content

fix(plugin-approvals): an out-of-office delegation applies only in its own organization - #25439

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-4a6wrl
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-4a6wrl

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25280

Clause-②: no (narrowing)
Metadata-protocol: none

What changed

ApprovalService.lookupActiveDelegation now reads a sys_approval_delegation row the way grantAppliesInTenant (@objectstack/core, resolve-authz-context.ts) reads a stored position or permission-set grant: the row reroutes an approver only when its organization_id is the request's organization, and both must name one.

Delegation row Request Before After
org A org A reroutes reroutes
org A org B no no
no organization (NULL, absent, blank) any reroutes in every organization no, and a warn names the row
any no organization takes every active row no
  • organizationNamed() normalizes NULL, absent and '' to "none", as core's module-private grantOrganizationOf does. It is applied to both sides of the comparison.
  • An active organization-less row that the lookup skips is logged at warn. The structured fields are { delegationId, delegatorId, delegateId, requestOrganizationId }, and the message gives the remedy. Its author still sees the row in Setup, and nothing else says it has stopped applying.
  • The rule is the same in every posture (ADR-0131 D11). There is no posture branch.
  • sys_approval_delegation.organization_id help, which said "null = applies across tenants for this delegator", now says what the code does:
    • en is regenerated with node scripts/check-i18n-bundles.mjs --write.
    • zh-CN, es-ES and ja-JP are translated by hand. The es-ES and ja-JP leaves were byte copies of the old English, so their source-hash entries drop on extract.
  • The object docblock and content/docs/automation/approvals.mdx (Out-of-office delegation) state the organization scope.

Who is affected

Under a wall (group/isolated), only stored state is affected. No current write produces an organization-less delegation:

  • A session insert is stamped with the active organization (injectTenantOnInsert in sql-driver.ts).
  • An elevated insert with no organization is refused (resolveSystemInsertOrganization, D9).
  • An update that empties the column is refused (denyOrganizationRemoval, security-plugin-write-tenant-wall.ts).

What remains are rows stored before 18.0. The D10 ceremony has no anchor for this table under a wall (inventory fate 3), so it reports such a row and leaves it NULL. Before this change, that row rerouted every organization's approvals for its delegator, onto a delegate who may hold no membership there.

Under single, a delegation written on 18.x applies exactly as before. D9 stamps the Default Organization on both the delegation and the record, and the request takes its record's organization. Only a pre-ceremony organization-less row stops applying, and os migrate organization-ownership --apply attributes it.

The changeset declares the break (**BREAKING**, ADR-0087 not-required (no-migration-prescription) marker) and gives the operator remedy for each posture. Precedent: .changeset/24583-7a2-organization-less-grant-readers-retired.md.

Tests

New file ooo-delegation-organization.test.ts (18 cases). Each walled case runs under both isolated and group:

  • A's delegation does not reroute an approval in B, and the request lands in B.
  • The same-organization delegation still applies, with the audit comment unchanged.
  • The request's organization is its record's: a member signed in to A, opening on B's record, gets no reroute from A's delegation.
  • An organization-less row reroutes nothing in either organization and is reported once per lookup, with the named fields.
  • A blank or absent organization counts as organization-less.
  • Beside an organization-less row, the same-organization row is the one that applies.
  • A chain stops at an organization-less hop.
  • A request with no organization takes no delegation.
  • Under single, a Default-Organization delegation reroutes, and an organization-less row does not and is reported.

approval-service.test.ts: the pin "a null-org rule applies across tenants" is replaced by its inverse.

Fixtures that wrote an organization-less delegation and expected a reroute now write it in the request's organization:

  • admin-exemption-retired.test.ts: seedDelegation writes 't1'.
  • admin-rescue-named-slate.integration.test.ts: dlg_admin writes ORG, and sys_approval_delegation leaves the relaxTenantColumns list.
  • approval-notification-body.integration.test.ts: dlg_1 and dlg_2 write orgOfDeal(...), and sys_approval_delegation leaves the relaxTenantColumns list.

The three installDefaultOrganization suites that insert a delegation (actor-lookups-hold-ids, approval-notification-keyed-text, approval-requested-notification) pass unchanged. D9 stamps the Default Organization on both sides there.

scripts/engine-double-contract.pinned.json records the new test file's double (check-engine-double-contract --write). Its update/delete open with assertEngineUpdateDispatch/assertEngineDeleteDispatch, and its find holds the caller's limit.

Verification

  • pnpm --filter @objectstack/plugin-approvals exec vitest run: 95 files, 1232 tests, all passed. pnpm --filter @objectstack/plugin-approvals typecheck passed, including check:test-typecheck.
  • Ablation (scripts/ablation-replace.mjs): the old filter was restored in approval-service.ts, then the new file, approval-service.test.ts and the three re-stamped fixtures were run.
    • 10 tests went red: the organization-less, blank or absent, chain, no-organization-request and single organization-less cases, plus the replaced pin.
    • The cross-organization and same-organization cases stay green under the old filter, as expected: the old filter already kept a scoped row in its own organization.
    • After the restore, the file matched HEAD and git diff HEAD was empty.
  • node scripts/check-i18n-bundles.mjs: OK, all bundles in sync.
  • node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0.
  • Local gates (dispatch-gates --commands): on the first pass 95 of 99 exited 0. The other four were handled as follows:
    • check:engine-double-contract and check:objectql-double-limit: fixed (see above).
    • check:dual-build-cjs-loads and check:skill-examples: exit 3 because their prerequisite dists were missing. Those packages are now built.
  • The full re-run of the gates on the final head and the full dogfood battery (the (narrowing) arm) are running now. Their counts go in the os-dev-report on plugin-approvals: an approval delegation with no organization still applies across every tenant (the retired NULL-means-global reading, ADR-0131) #25280.

Acceptance notes


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK

…s own organization

Claude-Session: https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK
Co-authored-by: Claude <noreply@anthropic.com>
…stamp fixtures

The en bundle is regenerated from the corrected organization_id help; the
es-ES and ja-JP leaves, which were byte copies of the old English text, are
translated, so their source-hash entries drop. Fixtures that wrote an
organization-less delegation and expected it to reroute now write it in the
request's organization.

Claude-Session: https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK
Co-authored-by: Claude <noreply@anthropic.com>
…ganization's approvals

Claude-Session: https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK
Co-authored-by: Claude <noreply@anthropic.com>
…tes through the engine predicates

The fake engine's update and delete open with assertEngineUpdateDispatch /
assertEngineDeleteDispatch, and its find holds the caller's limit, so it is
no looser than ObjectQL. The pinned ledger records the new double.

Claude-Session: https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 23, 2026 13:34
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/automation/approvals.mdx (via sys_approval_delegation (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/automation/flows.mdx (via ApprovalService (symbol, a top-level class))
  • content/docs/data-modeling/objects.mdx (via sys_approval_delegation (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))

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

  • content/docs/releases/v16.mdx (via sys_approval_delegation (symbol, a field of const object enObjects; a field of const object esESObjects; a field of const object jaJPObjects; a field of const object zhCNObjects))
  • content/docs/releases/v17/17-6.mdx (via ApprovalService (symbol, a top-level class))

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
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 6 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 77392e901cce58316e803144971f2741ef049fc4 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 77392e901cce58316e803144971f2741ef049fc4

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c7cf6654b99ab2d9da5e75445bdbcb8b57c9bc0d
Local-runs: none
Scope: packages/plugins/plugin-approvals/src/approval-service.ts, packages/plugins/plugin-approvals/src/sys-approval-delegation.object.ts, packages/plugins/plugin-approvals/src/ooo-delegation-organization.test.ts, packages/plugins/plugin-approvals/src/approval-service.test.ts, packages/plugins/plugin-approvals/src/admin-exemption-retired.test.ts, packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts, packages/plugins/plugin-approvals/src/approval-notification-body.integration.test.ts, packages/plugins/plugin-approvals/src/translations/en.objects.generated.ts, packages/plugins/plugin-approvals/src/translations/es-ES.objects.generated.ts, packages/plugins/plugin-approvals/src/translations/es-ES.source-hashes.generated.ts, packages/plugins/plugin-approvals/src/translations/ja-JP.objects.generated.ts, packages/plugins/plugin-approvals/src/translations/ja-JP.source-hashes.generated.ts, packages/plugins/plugin-approvals/src/translations/zh-CN.objects.generated.ts, scripts/engine-double-contract.pinned.json + packages/core/src/security/resolve-authz-context.ts (grantAppliesInTenant, grantOrganizationOf), packages/objectql/src/engine.ts (resolveSystemInsertOrganization), packages/drivers/driver-sql/src/sql-driver.ts (injectTenantOnInsert), packages/plugins/plugin-security/src/security-plugin-write-tenant-wall.ts (denyOrganizationRemoval), packages/cli/src/utils/organization-ownership-inventory.ts (sys_approval_delegation, fate 3), packages/plugins/plugin-approvals/src/lifecycle-hooks.ts (bindDelegationWriteGuard), packages/plugins/plugin-approvals/src/pre-18-tenant-column.testkit.ts, packages/plugins/plugin-approvals/src/default-organization.testkit.ts, docs/adr/0131-total-organization-ownership-no-null-organization-id.md (D1, D9, D10, D11), scripts/check-engine-double-contract.mjs, .changeset/24583-7a2-organization-less-grant-readers-retired.md (precedent), and the two prose faces .changeset/25280-delegation-own-organization.md and content/docs/automation/approvals.mdx read for truth to the code

① Derived judgments

  1. The own-organization match, organizationNamed() on both sides — right. organizationNamed(value) returns null for null, undefined and '', else String(value): the same normalization core's module-private grantOrganizationOf applies to a grant row's value. The comparison rowOrg === requestOrg runs only after rowOrg === null has already been diverted to the warn-and-skip branch, so a match needs the row side non-null; and because requestOrg is null for a request with no organization while rowOrg is never null at the comparison, a null request matches nothing. That is grantAppliesInTenant's rule (organizationId !== null && organizationId === tenantId) read in the same order, with no posture branch, which is ADR-0131 D11. One deliberate non-fork: core's normalizer also reads a camelCase organizationId spelling off the row; the plugin's reads the value handed to it, and the delegation read names its columns (fields: [... 'organization_id']), so no camelCase row reaches it. No drift.

  2. The request's organization that feeds the comparison — right. Every applyOooDelegation call site (user, field, manager, expression resolveAs: user) receives the organizationId expandApprovers was given, and openNodeRequest passes requestOrg = subjectOrg ?? ctxOrg (the subject record's organization through the shared recordOrgResolver, the acting context as fallback); the fallback and admin-rescue slates pass the same requestOrg, and the decide-time re-expansion passes the stored request row's organization_id. The new pin "a member signed in to A, opening on B's record, gets no reroute from A's delegation" holds that reading.

  3. An org-less active row warns and never matches; is a legitimate delegation lost? — no, the dev's reach reading holds against the cited code. Under a wall: a session insert carries options.tenantId and injectTenantOnInsert fills an empty organization_id from it; an elevated or system insert with no organization reaches Engine.resolveSystemInsertOrganization, which derives the Default Organization only under single and otherwise throws SystemWriteOrganizationRequiredError (D9); an update that empties the column is refused by denyOrganizationRemoval in both the payload half and the stored-image half of the write wall, gated on walled. The delegation write guard (bindDelegationWriteGuard) stamps only delegator_id and does not touch the organization, so it neither helps nor hurts. So under a wall the only organization-less rows are stored state from before, and organization-ownership-inventory.ts:538 records sys_approval_delegation as fate: 'attribution' with no anchor and the D10_SINGLE citation ("under single, the Default Organization; otherwise reported"), which is exactly what the PR says: attributed under single, reported and left NULL under a wall. Under single, D9 derives the Default Organization for the delegation its author writes and for the request's subject record, so an 18.x delegation still applies by construction, and the three installDefaultOrganization suites that insert a delegation without an organization (actor-lookups-hold-ids, approval-notification-keyed-text which now writes ORG explicitly, approval-requested-notification) pass unchanged on the head's check-runs. Two populations sit outside the PR's "only a database upgraded from before 18.0" sentence and are judged non-blocking: a lean embedding that registers no sys_organization (the D9 2026-10-22 note stores NULL there, by design, and no served production composition reaches it), and a single store born on 18.0.0 (tenant columns still nullable per the D1 amendment) where an update clears the column outside any wall. In both the row is reported at warn with the os migrate organization-ownership --apply remedy, which attributes it under single, so nothing is silently lost.

  4. Every delegation reader is covered — right. On the head, sys_approval_delegation is read in exactly one place, lookupActiveDelegation (engine.find at approval-service.ts:3278), and lookupActiveDelegation has exactly one caller, applyOooDelegation. The notification body, the admin-rescue slate and the decide-time re-expansion all consume applyOooDelegation's output (the substitutions collector or the returned slate), never the table. lifecycle-hooks.ts names the object only for its write guard and its hook-package list; approvals-plugin.ts for the nav item; packages/client/src/index.ts in a docblock. No second door applies a cross-organization delegation.

  5. The warn — right, and shaped as the card asked. It fires per skipped active org-less row per lookup, carries { delegationId, delegatorId, delegateId, requestOrganizationId } and the two-posture remedy; this.logger?.warn?. matches the file's other warnings. The new pin asserts the kind and the named fields, never the wording.

  6. scripts/engine-double-contract.pinned.json — right. The two new entries (ooo-delegation-organization.test.ts, verbs delete and update, pinned: 1 each) match the test double, whose update opens with assertEngineUpdateDispatch(data, options) and whose delete opens with assertEngineDeleteDispatch(options); find honours options.limit. The ledger counts pinned doubles per (file, verb), so 1 is the right count. Lint and Repo Gates is green on the head.

  7. The approvals.mdx paragraph — true to the code. "Belongs to the organization it is created in" is injectTenantOnInsert/D9; "reroutes only the approvals opened in that organization" is the comparison; "a request's organization is its record's" is subjectOrg ?? ctxOrg; "create a delegation in each" follows; "reroutes nothing, and the approval service logs a warning" is the skip branch. The one broad word is "only a database upgraded from before 18.0 can hold" an org-less row (see item 3's two edge populations); not false for any served production composition, and judged non-blocking.

  8. The object help and the four bundles — right. en matches the object's new description byte for byte (regenerated). es-ES ("La organización a la que pertenece esta delegación. Solo redirige las aprobaciones abiertas en esta organización."), ja-JP ("この委任が属する組織です。この組織で開始された承認だけを振り替えます。") and zh-CN ("此委派所属的组织。它只改派在该组织中发起的审批。") each say what the English says. The es-ES and ja-JP source-hash entries for objects.sys_approval_delegation.fields.organization_id.help drop, which is what the companion's own header prescribes for a leaf that stops being a byte copy ("Re-translate the leaf ... the next extract drops its entry by itself"); zh-CN already carried no entry for that path. The claim (6365085992) said es-ES/ja-JP would be left as the tool's byte copies; the PR hand-translated them instead and says so in its body. A stricter outcome than the claim, not a breach.

  9. The edited tests kept what they pinned — right. admin-exemption-retired: seedDelegation now writes organization_id: 't1' (the submitter's organization in that suite) and the suite's assertion, that a delegation cannot rescue an in-flight approval, is untouched. admin-rescue-named-slate: dlg_admin writes ORG and sys_approval_delegation leaves the relaxTenantColumns list, so the table keeps its NOT NULL column and the insert must carry the organization, which it does; the assertion (an out-of-office administrator is covered by their delegate) is untouched. approval-notification-body: dlg_1/dlg_2 write orgOfDeal('ooo')/orgOfDeal('ooo_gone') and the table leaves the relaxed list; the notification-body assertions are untouched. approval-service.test: the one pin that stated the retired reading ("applies a cross-tenant (null org) rule regardless of request tenant") is replaced by its inverse and additionally asserts no ooo_substitute audit action was written. Nothing was loosened; each re-stamp moves the fixture onto the organization the request lands in.

  10. The new pin file carries the card's acceptance. Under isolated and group: A's delegation does not reroute in B and the request lands in B; the same-organization delegation still applies with the audit comment unchanged; the record's organization wins over the acting context's; an org-less row reroutes nothing in either organization and is reported once per lookup with the named fields; blank and absent count as org-less; the same-organization row applies beside an org-less one; a chain stops at an org-less hop; a request with no organization takes none. Under single: the Default-Organization delegation reroutes; an org-less row does not and is reported. The single org-less case is the fate-3 "stored state from before the ceremony" population, correctly framed.

  11. Public surface. No export is added or changed: organizationNamed is module-private; lookupActiveDelegation stays private; packages/core and packages/spec are untouched; no schema, no governed path. What narrows is which stored rows reroute an approval. The accept set of sys_approval_delegation is unchanged (every row a caller could write before is still accepted).

② Semver level

  • Changeset .changeset/25280-delegation-own-organization.md: @objectstack/plugin-approvals: patch, Clause-②: no (narrowing), Metadata-protocol: none, one adr-0087: not-required (no-migration-prescription) marker, a **BREAKING** banner, FROM/TO stated in prose and the operator remedy per posture. The PR body carries the same two declaration lines. Right.
  • no: no published accept set or public surface widens (item 11). (narrowing): a published behaviour is withdrawn, the help text "null = applies across tenants for this delegator" and the reroute it described. Metadata-protocol: none: sys_approval_delegation holds runtime data, no authorable key moves. Per AGENTS.md ("the level follows the kind alone ... none takes patch; a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition") the level is patch. Right.
  • Precedent .changeset/24583-7a2-organization-less-grant-readers-retired.md is yes (narrowing) at minor because it moved exported readers in @objectstack/core and @objectstack/plugin-security (public surface); this PR moves none, so no (narrowing) at patch is the consistent reading of the same break on a package that exports nothing new.
  • ADR-0087 marker: the category exists in ADR-0087's closed vocabulary; the body's FROM/TO is prose with no arrow and no code-span rewrite pair, so the catch-all is not refused by the prescription detector, and Lint and Repo Gates (which runs check:adr-0087-registration) and Check Changeset are both green on the head. Right.
  • The narrowing face: scripts/pm/record-recognisers.mjs --paths on the file list decides no path face and defers to the changeset's Clause-②: line, which carries (narrowing), so --face narrowing and CONTRACT_REVIEW_TIER is the served tier. Consistent with the brief.

③ Boundary flags

  • open_questions: [] in the os-dev-report (6367007163). out_of_scope_findings: []. Nothing to answer.
  • Dev flag, PR body "Acceptance notes": the branch is the harness-assigned claude/project-thread-4a6wrl, not claude/issue-25280-.... The claim comments (6364952044, 6365085992) are the identity record and name that branch; the check-run "The card this PR closes must claim this branch" is green. Answered.
  • Dev flag: Delegations (OOO): the Delegator can only be the signed-in user, but the field starts at "Select…" and a search for a colleague answers "No options found" with no reason — prefill it and say why (hotcrm#2194 R2-T2-U04) #25342 (delegator prefill) edits the same object file and bundles; whichever lands second merges main and regenerates. "No other open PR may claim the same single-writer path" is green on the head. Answered, no action here.
  • Dev flag: no automated rewrite is offered for a walled org-less row because the row names nothing to attribute it by. Matches inventory fate 3 for this table (no anchor). Answered.
  • Dev flag (claim, superseding): the (narrowing) arm owes one full dogfood battery on the final head. The head's check-runs show Dogfood Regression Gate success with the three shards, Console Pin Gate, Temporal Conformance and Dogfood Verify CLI skipped by design; the full battery evidence is the os-dev-report's dogfood_battery line (os-verify-lock on c7cf6654b, 290 files passed, 3 skipped, 2494 tests passed, 18 skipped, no failures). Recorded as the dev's measurement; the check-runs on the head answer every derived gate family.
  • Reviewer observation, non-blocking: the "only a database upgraded from before 18.0 can hold" sentence (docs and changeset) is broader than the code guarantees for two populations outside any served production composition (item 3); the warn and its remedy cover both. No change asked.
  • Check-runs on c7cf6654b99ab2d9da5e75445bdbcb8b57c9bc0d: 42, every one success or skipped by design; none failed.

Implemented-by: claude/project-thread-4a6wrl
Reviewed-by: session_016g7nYP9wdNx1GDa5XDr7Uy

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-23T14:27Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25280 (M, card thread; claim 6365085992, which supersedes 6364952044; os-dev-report 6367007163) · reviewed head c7cf6654b9

  • Contract review: record 6367199738 on this head, served at CONTRACT_REVIEW_TIER, PASS, Local-runs: none. Its scope is all 14 non-prose paths plus the code they cite: core's grantAppliesInTenant/grantOrganizationOf, injectTenantOnInsert, resolveSystemInsertOrganization, denyOrganizationRemoval and the ownership inventory.
  • Diff: 16 files, +389 / −20.
    • ApprovalService.lookupActiveDelegation now applies a delegation only when the row's organization and the request's organization are both non-null and equal. This holds in every posture (ADR-0131 D11), using the same normalisation as core's grant reader.
    • An active row with no organization is skipped, with a structured warn that names the remedy.
    • lookupActiveDelegation is the only reader of sys_approval_delegation rows, and its sole caller is applyOooDelegation. So the notification body, admin rescue and reassign all inherit the fix, and no second door is left.
    • The four edited existing tests had only their seeded organizations corrected. Their assertions are untouched, apart from the one pin that stated the retired reading, which is replaced by its inverse. Nothing was loosened.
    • The new ooo-delegation-organization.test.ts has 18 cases covering isolated, group and single.
  • Side changes:
    • The engine-double-contract.pinned.json entries were written by the gate's --write for the new double, which dispatches through assertEngine*Dispatch.
    • The approvals.mdx paragraph is true to the code.
    • The es-ES and ja-JP help texts are real translations. Their source-hash drops are what the tool writes for a re-translated leaf.
  • Pins and battery (report):
    • plugin-approvals: 1232/1232 pass. The ablation (old filter restored) turns 10 pins red, and the restore is clean.
    • Gates: 108/108 exit 0, with 0 unrun.
    • The narrowing arm's full dogfood battery ran once on this head under os-verify-lock: 290 files / 2494 tests pass, 18 skipped, lock VERDICT exit 0.
    • CI skips the dogfood shards by design on this path set. The seat therefore adopts the dev's battery measurement as the battery of record.
  • CI on this head: 42 check-runs, all success or skipped by design. git merge-tree onto today's main (after fix(plugin-approvals): a new delegation starts with the Delegator set to you, and its help says why #25420, the sibling on the same object file) is clean.
  • Declarations: Clause-②: no (narrowing), patch, Metadata-protocol: none. There is one adr-0087: not-required (no-migration-prescription) marker and a BREAKING banner with a per-posture remedy. This matches the record's ②: no export moves, so it is not yes.
  • Non-blocking, from the record: the docs and changeset say only a database upgraded from before 18.0 can hold an org-less row. That is slightly broader than the code: a lean embedding with no sys_organization and an 18.0.0-born single store can also hold one. Both populations get the same warn and the os migrate organization-ownership --apply remedy, so no reader is misled about what to do. This is left as wording, not a defect.
  • Body: the attribution header, the os-sales assignee and reviewer, and the closing footer are present.
  • Commit trailers: model-free Co-authored-by: Claude + Claude-Session on every authored commit. The one trailer-less commit is a merge of main.
  • Landing: not governed (check-governed-merges --pr 25439: 409 lines). Ready + auto-merge through the relay. Fixes #25280 closes the card on merge.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 23, 2026 14:27
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 23, 2026 14:27
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit b3fddd1 Oct 23, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-4a6wrl branch October 23, 2026 15:06
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/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-approvals: an approval delegation with no organization still applies across every tenant (the retired NULL-means-global reading, ADR-0131)

2 participants