Skip to content

Promote resolveRecordOrganizationField to the shared platform-row resolver: approvals + automation runs stamp the SUBJECT record's organization (cloud#1395 Option A) - #11311

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-10101-shared-platform-row-org-resolver
Aug 23, 2026
Merged

Promote resolveRecordOrganizationField to the shared platform-row resolver: approvals + automation runs stamp the SUBJECT record's organization (cloud#1395 Option A)#11311
os-sam merged 3 commits into
mainfrom
claude/issue-10101-shared-platform-row-org-resolver

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#10101

Implements the maintainer ruling recorded on cloud#1395 (2026-08-17T03:18Z, 「新进卡六张 同意你的建议」), transcribed into the spec annotation by #10999:

Ruled: Option A — extend the #8778 ruling: resolveRecordOrganizationField is promoted to a shared resolver used by all three platform-row writers (approvals, automation runs, audit). A platform row's organization is the SUBJECT record's organization; actor context is the fallback, never the primary.

⚠️Clause-② is yes (fixed by the ruling): this PR is a draft and STAYS a draft; needs:contract-review stays on the card. No ready-flip, no auto-merge.

Where the shared resolver lives, and why

@objectstack/metadata-core, new module record-organization.tsresolveRecordOrganizationField moved verbatim from plugin-audit, plus createFieldPresenceProbe (its companion) and a new memoized factory createRecordOrganizationResolver (its organizationOf method takes an object name plus candidate records and returns the subject record's organization as a string, or null when unresolvable). This is the package's documented criterion for exactly this situation — the #5619 engine dispatch predicates, the ADR-0106 FLS projection and the #6190 org-scope predicate were each "sunk here" because the consumers span packages with no other common home, and metadata-core depends on @objectstack/spec plus zod only, so plugin-audit, plugin-approvals and service-automation all import it with no new cycle. Placing it in @objectstack/objectql instead would have forced two writer packages to take the whole query engine as a dependency for one pure function; packages/spec is barred (no business logic, and zero spec ownership in this lane). ⛔ There is ONE implementation; plugin-audit re-exports from its original paths (public surface unchanged), so this is not Option B by any door.

The three writers

  • audit (plugin-audit/src/audit-writers.ts): imports the shared resolver; local definitions deleted; behavior identical (its 313-test suite is untouched and green).
  • approvals (plugin-approvals/src/approval-service.ts, openNodeRequest — the only sys_approval_request insert site): resolves requestOrg as the subject record's organization (live record first, trigger snapshot second) with the previous ctxOrg as fallback. requestOrg feeds the request row, both sys_approval_action inserts, the sys_approval_approver index AND the approver-slate expansion — all the places that mean "this request's organization" move together; a slate resolved in a different organization than the wall the request lands behind would be approvers who cannot see the request they are asked to decide.
  • automation runs (service-automation): both write paths of ObjectStoreSuspendedRunStore resolve from the trigger-record snapshot with the acting tenant as fallback — serialize() for paused rows, recordTerminal() for terminal rows (which previously carried NO organization at all: nothing ever set RunRecord.organizationId). The engine now threads the run context into recordLog so the terminal record carries the two attribution inputs (acting tenant + trigger-record snapshot; RunRecord.triggerRecord is a write-time input, not a column). Same inputs, same precedence on both paths, so a run's paused row and its terminal row agree by construction — pinned by a test.

The sys_api_key divergence — explicitly NOT flattened

tenancy.organizationField answers who a row is ABOUT; tenantField / organization_id answers what an object is WALLED by. They deliberately diverge for credential tables (#8287). Pinned three times: at the resolver (metadata-core unit tests — limb 0 wins over the ADR-0066 opt-out, and the missing-column guard falls through), at the approval writer (stamps active_organization_id; and an ADR-0066 platform-global object with an org FK but no organizationField does NOT stamp from the FK), and at the automation store. Nothing here touches applyTenantScope or any wall.

Both directions pinned (the fallback survives)

Every writer's suite pins: subject beats actor when both resolve; actor answers when the subject is unresolvable (no org column, no record, no schema access on a test double); tenant-less AND subject-less stays NULL — fabricating an acting organization for schedule/api triggers stays vetoed (Option C).

The three promote, never repair pins

The framework pin is PROMOTED in this PR per its own instruction: suspended-run-store.test.ts's pinned-defect test now asserts the organization resolved from the trigger record (reverse-verified — see below). The two cloud-side pins (hotcrm-multitenant.acceptance.ts, check a4 in verify-hotcrm-saas.mjs) are tracked on cloud#1395 and follow at the next .objectstack-sha bump — the framework side has landed once this merges; ping the repo:cloud seat (objectstack#6026). This session cannot reach the cloud repo (measured constraint).

The #10131 backfill question — answered: DECLINED, with reasons and a filed card

The measured stranded population (9/9 sys_approval_request, 12/12 sys_automation_run org-less rows naming subjects that have organizations) is NOT repaired by this PR. A backfill writes to existing production rows — its own blast radius, which should not ride a contract-review-tier behavior change. Cost of declining, stated: pending org-less approval rows keep locking their records invisibly until repaired (in-product recovery stays the #3424 admin override); terminal automation rows age out anyway under the object's declared 30-day retention; paused rows stay unattributed until resumed. Filed as #11308 (blocked by this PR: a backfill must use this same shared resolver, never a second one).

Out-of-scope finding, filed: the spec annotation's prose is now stale by its own design

The #10999 annotation says the .describe() "still speaks of audit rows … and #10101 updates it as the readers actually land" — but this lane has zero packages/spec ownership (its spec slice was exactly #10999), so that prose refresh is filed as #11307 for the spec seat rather than ridden here. The annotation's load-bearing property — exactly THREE sanctioned consumers, "and no others", a fourth needs its own ruling — is untouched, and the moved resolver's documentation restates it.

Also in this diff

  • The reworded organization_id field descriptions (ADR-0120 terminology; the old text claimed a propagation that measurably did not happen). i18n bundles regenerated (check:i18n OK, 9/9 packages); zh-CN / ja-JP / es-ES hand-translated in the same pass.
  • plugin-audit and plugin-approvals now resolve @objectstack/metadata-core to SOURCE in their tsc and vitest programs (tsconfig paths + anchored vitest aliases; rootDir widened per the packages/rest[finding] protocol.deletePackage has no declared spec shape — three hand-rolled types that disagree, and the runtime twin reaches it through as any #9960 precedent) — check:test-source-alias and check:type-source-resolution both demanded it for the new dependency edges and are green.
  • Changeset: metadata-core minor (new exports), the three writers patch. Not declared-breaking; no ADR-0087 disposition required.

Verification (all at the final commit 45393d212b, clean tree, exit codes captured before any pipe)

  • Tests: metadata-core 12 files / 177 passed · plugin-audit 19 / 313 · service-automation 84 / 1005 · plugin-approvals 30 / 570 — one consolidated run, verdict line os-verify-lock: VERDICT command-exit 0.
  • Typecheck: metadata-core, plugin-audit, plugin-approvals green (service-automation declares no typecheck script — pre-existing ledger state).
  • Reverse verification, direction predicted then observed, mutation and restore both confirmed on disk by anchored grep counts: reverting the store's serialize() resolution to actor-only turned exactly the 4 subject-first automation tests red (28 fallback pins stayed green); reverting requestOrg to ctxOrg in openNodeRequest turned exactly the 4 subject-first approval tests red (14 stayed green). Both mutation scripts carried trap-based restore; both suites re-ran green after restore. No dist leg applies: these suites resolve the mutated files from source (vitest in-package relative imports).
  • Gate union derived by node scripts/pm/dispatch-gates.mjs (no hand-supplied paths) at 45393d212b; every named family run to a real verdict, all green: changeset-gate-self-tests · objectui-changeset · override-consistency · published-files · slot-lookup · test-source-alias · type-source-resolution · adr-0087-registration · changeset-fixed · changeset-no-major · ci-filter-parity · empty-changeset · osv-exemptions · plugin-teardown-shape · affected-docs · query-options-erasure · where-matcher · engine-double-contract · cross-package-test-inputs · i18n · nul-bytes · type-check-coverage · type-check-debt (re-measure OK after rebuilding the 7 packages its preflight named). The two type-check ratchets ran on the byte-identical tree the final commit captured (zero dirty files at commit time); everything else re-ran after the commit.
  • No new engine doubles: the two touched fakes are pre-existing doubles extended with an optional schema map (check:engine-double-contract OK, no ledger change).

Generated by Claude Code


Generated by Claude Code

…d platform-row resolver (#10101)
Implements the cloud#1395 Option A ruling: a platform row's organization is
the SUBJECT record's organization; actor context is the fallback, never the
primary. The resolver moves from plugin-audit to @objectstack/metadata-core
(the {spec, zod}-only common home all three sanctioned writers can import
with no new cycle); plugin-audit re-exports from its original paths, the
approval-request writer stamps subject-first in openNodeRequest, and the
automation-run store resolves the trigger record's organization on both its
write paths (paused serialize() and terminal recordTerminal()).
WIP: tests, i18n bundle regeneration and changeset follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
#10101)
- metadata-core: unit pins for the four-limb precedence, the sys_api_key
divergence, and the memoized resolver factory (12 tests)
- service-automation: the PINNED defect test is PROMOTED per its own
instruction; subject-beats-actor, fallback directions, Option-C veto,
sys_api_key divergence, paused/terminal agreement, and an engine-to-store
end-to-end handoff pin
- plugin-approvals: openNodeRequest attribution pins (row + action + approver
index move together, both fallback directions, sys_api_key divergence and
the ADR-0066 limb-1 non-stamp)
- i18n: reworded organization_id help extracted to the en bundle; zh-CN /
ja-JP / es-ES hand-translated in the same pass (check-i18n-bundles OK, 9/9)
- changeset for the four packages
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
…est programs (#10101)
check:test-source-alias and check:type-source-resolution both red on the new
@objectstack/metadata-core edges: without these entries plugin-audit's and
plugin-approvals' typecheck and test verdicts would be about metadata-core's
dist build state rather than about the source in the checkout. paths + anchored
vitest aliases per the gates' own prescription; rootDir widened as the
packages/rest #9960 precedent records.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APWX2AwT3a4xDcjPCe8bk4
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata-core, @objectstack/plugin-approvals, @objectstack/plugin-audit, @objectstack/service-automation, touching 27 documentable anchor(s).

6 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_request (symbol))
  • content/docs/automation/flows.mdx(via ApprovalService (symbol), sys_approval_request (symbol))
  • content/docs/automation/workflows.mdx(via sys_approval_request (symbol))
  • content/docs/concepts/architecture.mdx(via getSchema (symbol))
  • content/docs/plugins/packages.mdx(via sys_approval_request (symbol))
  • content/docs/ui/translations.mdx(via sys_approval_request (symbol))

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

  • content/docs/releases/v16.mdx(via AutomationEngine (symbol), sys_approval_request (symbol))
  • content/docs/releases/v17.mdx(via AutomationEngine (symbol), active_organization_id (literal))

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
  • 7 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts, packages/plugins/plugin-approvals/tsconfig.json, packages/plugins/plugin-approvals/vitest.config.ts, …) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 13 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 ab23c67abc2c99db9b58e70d46c438cd2c47151cpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 6ec455c44a76551ed430013f2cc690bd3f02d3c6 — the merge of head 45393d212b1207df73f026a0ff3c0e8c6257e0cc into base ab23c67abc2c99db9b58e70d46c438cd2c47151c, 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 6ec455c44a76551ed430013f2cc690bd3f02d3c6 && git checkout 6ec455c44a76551ed430013f2cc690bd3f02d3c6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab23c67abc2c99db9b58e70d46c438cd2c47151c 45393d212b1207df73f026a0ff3c0e8c6257e0cc && git checkout -B drift-repro ab23c67abc2c99db9b58e70d46c438cd2c47151c && git merge --no-ff 45393d212b1207df73f026a0ff3c0e8c6257e0cc
node scripts/docs-audit/affected-docs.mjs --json ab23c67abc2c99db9b58e70d46c438cd2c47151c

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

@claude

claudeBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

合同审核已通过(PASS),判定记录见卡 #10101needs:contract-review 已清除(卡侧;PR 侧本就未挂标)。按 clause ② 本 PR 为 draft 交付、人工 merge;merge 后请 ping objectstack#6026(repo:cloud 席)跟进 cloud 侧两枚 pin 的 promote(挂账在 cloud#1395,等 .objectstack-sha bump)。


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 23, 2026 12:51
@os-sam
os-sam added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit 1272f0aAug 23, 2026
33 checks passed
@os-sam
os-sam deleted the claude/issue-10101-shared-platform-row-org-resolver branch August 23, 2026 13:08
os-steve pushed a commit that referenced this pull request Aug 23, 2026
Refresh the #8778 scope-pin annotation and the .describe() beside
tenancy.organizationField in packages/spec/src/data/object.zod.ts. The
annotation (landed by #10999) said consumer 1 (audit stamping) was "still
the only one wired up" and consumers 2/3 (approvals, automation runs) were
"sanctioned but not yet implemented". #10101's PR #11311 (merged
2026-08-23) landed both: resolveRecordOrganizationField was promoted to a
shared resolver in @objectstack/metadata-core and all three sanctioned
platform-row writers now call it. Also corrects the resolver-home naming
("plugin-audit's resolveRecordOrganizationField" -> @objectstack/metadata-core's,
plugin-audit re-exports).
Prose accuracy only: the pin's load-bearing "exactly THREE consumers, and
no others" sentence and the fourth-consumer refusal posture are unchanged
verbatim. No accept/reject behaviour change, no schema shape change, no
new keys.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Promote resolveRecordOrganizationField to the shared platform-row resolver (approvals + automation runs), per the ruled cloud#1395 Option A

2 participants

@os-sam@claude