Repository navigation
fix(metadata-protocol)!: an organization's seed load resolves people through its membership, reports a refused reference probe, and writes as that organization - #33222
Conversation
…membership and carry the organization to hooks WIP: probe scope per target, refused probes reported, per-load write context. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzSAkofrHDU1U8yFeA
…hared bag Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzSAkofrHDU1U8yFeA
…nization Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzSAkofrHDU1U8yFeA
… changeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzSAkofrHDU1U8yFeA
…azy metadata import in the pins Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzSAkofrHDU1U8yFeA
…-e226dr Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzSAkofrHDU1U8yFeA
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 28c03049ab68f9abb90f5901970874b4f0ab4fa6 && git checkout 28c03049ab68f9abb90f5901970874b4f0ab4fa6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e540baf8873c22642ce191b99e7e5ce1fd3b4620 41de6f56d8797f55610ae48e799270eac4891c84 && git checkout -B drift-repro e540baf8873c22642ce191b99e7e5ce1fd3b4620 && git merge --no-ff 41de6f56d8797f55610ae48e799270eac4891c84
node scripts/docs-audit/affected-docs.mjs --json e540baf8873c22642ce191b99e7e5ce1fd3b4620
|
|
domain:server#7 · os-tesla · session_01L9qibzSAkofrHDU1U8yFeA Contract reviewServed-tier: Inputs read: cards #33207 and #33206 (bodies and all five comments each, the claim amendments 6871524022 and 6871533047 included), PR #33222 (body, four files, net diff against ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe narrowing itself holds against the published spec: it does not take a declared capability away at the consumer.
Finding 1 is a must-fix, so the round fails; findings 1 to 3 each earn a delta review of their fix on the new head. Implemented-by: VERDICT: FAIL Generated by Claude Code |
…lared-global objects, write a row as its own organization The person probe on an organization's load read one unordered page of same-named people, so a member past it was missed and called a non-member. It now reads the candidates in id order, page by page, and says "not a member" only after every candidate was asked; a refused membership read is reported on sys_member. An object with no organization_id column is probed unscoped only when its author declared it global (tenancy.enabled false): an object walled by another tenant column keeps the filtered probe and its reported refusal. A row that supplies its own organization is written with that organization on its write context, its update and its pass-2 back-fill included, so a walled by-id write finds the row. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzSAkofrHDU1U8yFeA
…field and report a back-fill that matched no row An author's object with no organization_id column is probed unscoped on an organization's load only when it resolves no tenant field at all, by the same reading the wall scopes by. A pass-2 back-fill whose by-id update answers null matched no row: it is reported as a failed back-fill, never counted as resolved. The changeset and the seed-data page name the whole narrowed member: every read and by-id write a seed write performs or triggers is scoped to the organization the row is written as. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzSAkofrHDU1U8yFeA
|
domain:server#7 · os-tesla · session_01L9qibzSAkofrHDU1U8yFeA Contract reviewServed-tier: Delta review of round 1 (FAIL on ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentRound 1 findings, each judged on this head:
New findings on the delta:
No must-fix and no should-fix on this head. Nits 5 to 7 end the round; none requires a change that moves the head. Implemented-by: VERDICT: PASS Generated by Claude Code |
…t a wire code The refusal is reported through the back-fill's own error path; it never reaches a dispatcher, so it carries no unregistered code. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzSAkofrHDU1U8yFeA
|
domain:server#7 · os-tesla · session_01L9qibzSAkofrHDU1U8yFeA Contract reviewServed-tier: Second delta review, on top of the round-2 PASS at ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#7 · os-tesla · session_01L9qibzSAkofrHDU1U8yFeA Contract review brief — PR #33222This covers the three contract-review records on this PR: 6878779800 (round 1, FAIL, head Deviations from
|
|
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx 维护者速读 · PR #33222 @
|
domain:server#7 · os-tesla · session_01L9qibzSAkofrHDU1U8yFeA
Requested by Tesla · project thread
Fixes #33207
Fixes #33206
Clause-②: yes (narrowing) — narrowed: under a walled tenancy posture (
isolatedorgroup), every read and by-id write a seed write performs or triggers (a hook'sctx.apiread, the engine's own reads on that row, the loader's update of an existing row and its pass-2 back-fill) is scoped to the organization the row is written as, where it reached every organization's rows. Widened: on a load for one organization, a person reference resolves to a member of that organization,sys_organizationto the load's own, an object no organization walls (noorganization_idcolumn and no tenant field) across organizations, and a hook on a seeded row sees the organization the row is written as. Nothing that resolved before stops resolving; no load that succeeded is refused.Metadata-protocol: none
Contract review at CONTRACT_REVIEW_TIER (narrowing): round 1 FAIL on
3c36f9c6e(one must-fix, two should-fix, three nits). The fixes are on this head; the same reviewer owes the delta review before the seat lands it.维护者速读
organizationId。seed-loader.ts,回滚即还原该文件。packages/metadata-protocol/src/seed-loader.ts的resolveFromDatabase/probeScopeOf/probeOrganizationMember,以及loadDatasets里seedOptions的赋值。pnpm --filter @objectstack/runtime exec vitest run src/seed-org-scoped-references.integration.test.ts,预期 17 个用例全绿。What changes
#33207: references on an organization's own load.
resolveFromDatabaseAND-scoped every probe withorganization_id,sys_userincluded.sys_userhas no such column, so the SQL driver refused the probe (INVALID_FILTER), the barecatchread it as "no match", and the reference was reported as "no such sys_user row exists". Measured onmain6e1ab9b with the real AppPlugin replayer, ObjectQL and SqlDriver: the single-tenant boot resolvedrep: 'Zhang Wei'tousr_zhang; both per-organization replays left itnull, the member's organization included.Each target is now judged once per load (
probeScopeOf) from the definition the stamp already reads:organization_idcolumn (declared or injected), or cannot be described: filtered to the load's organization, as before;sys_user: candidates by the field, read in id order page by page (50 a page) until one holds asys_memberrow in the load's organization (probeOrganizationMember). A same-named person outside it is never used, and "not a member" is said only after every candidate was asked;sys_organization: the load's own organization only;managedBy) and resolving no tenant field at all (resolveTenantFieldName, the reading the wall scopes by): global, unscoped. An object walled by another tenant column (tenancy.tenantField) and platform-managed tables without the column keep the old probe, and its refusal is now reported.A load with no organization probes unscoped, exactly as before.
The narrowed catch. A refused probe records its code and the chain moves on. The load is never refused for it. One
warnper target, field and code per load; the pass-2 error keeps its first sentence and appends the refusal's code (or, for a person outside the organization, that the person is not a member) instead of claiming no row exists.#33206: the organization in the write context.
ExecutionContexthas noorganizationIdkey: a hook'ssession.organizationIdand itsctx.apiscope are built fromtenantId(measured: the card's{ ...SEED_WRITE_EXECUTION_CONTEXT, organizationId }reaches no hook). The loader now builds its options bag per load:{ context: { ...SEED_WRITE_EXECUTION_CONTEXT, tenantId } }, withtenantIdthe organization the load stamps its rows with (config.organizationId ?? fallbackOrgId, the seat's ruling on the shape note). A row that supplies its own, differentorganization_idis written as that organization (writeOptionsFor): its insert or update, and its pass-2 back-fill, so a walled by-id write finds the row; a batch holds rows of one write organization.SEED_WRITE_EXECUTION_CONTEXTis unchanged. A load with no organization, pinned or derived, uses the frozenSEED_OPTIONSbag itself. The landed-key ledger insert uses the per-load bag.A pass-2 back-fill whose update answers
null(the data-engine contract's "matched no row") is reported as a failed back-fill, never counted as resolved. ObjectQL itself already refuses a by-id update it cannot find with a not-found error, which the back-fill reports.skipTriggersdoes not stop hooks: it reaches only the record-change trigger's flow dispatch, andtriggerHooksskips metadata hooks only onskipAutomations. So the card's premise stands: app hooks run on seed writes.Tests
New
packages/runtime/src/seed-org-scoped-references.integration.test.ts(real AppPlugin, its walledseed-replayerand itssingleinline seed, SeedLoaderService, ObjectQL, SqlDriver over better-sqlite3), 17 cases:repandowner_id);tenantField: 'workspace_id'is not taken for global (another organization's row is not borrowed); the organization table answers only for the load organization;Globexis not borrowed by North);singleboot (sole organization) and a direct pinned load (the install path);sessionequals{ isSystem, skipTriggers });isolatedand undergroup, a hook'sctx.apiread on a seeded row sees only the load organization.And one case in
packages/metadata-protocol/src/seed-loader-deferred-failure.test.ts: an engineupdateansweringnullon the back-fill is reported aterrorwith "matched no row" and is not counted as resolved.Ablations (every negative pin, plus the two positive halves): mutate, rebuild, marker proved in dist/, run, restore
Each leg through
scripts/ablation-replace.mjs(anchor must hit, restore proved by blob hash),OS_SKIP_DTS=1 pnpm --filter @objectstack/metadata-protocol build,scripts/ablation-dist-preflight.mjs(marker present in 2 built files), then the file above.singlehook, install hook, declared narrowingsys_userprobed with the organization filter (main)globalwithout the tenant-field readingworkspace_idobject not borrowedisolatedandgroupnullanswer not checkedEvery leg:
ok restored: blob == HEADandgit diff HEADempty (r0 legs at blob 337629ad067a, r1 legs at de0e52ac36a2 / c3371fee5f33). Restore leg r1: rebuilt, markers G H I J E--absentfrom dist/, file 17/17 green, tree clean.Suites and gates
pnpm --filter @objectstack/metadata-protocol exec vitest run: r1 Test Files 339 passed, 9 skipped; Tests 22844 passed, 43 skipped. No existing seed-loader pin needed a change.pnpm --filter @objectstack/metadata-protocol typecheck,pnpm --filter @objectstack/runtime typecheck(withcheck:test-typecheck): exit 0, after a declaration build of the runtime closure.pnpm lint: exit 0.node scripts/pm/dispatch-gates.mjs --commands: 94 lines, each run with stdin closed. 90 exit 0 on first run;check:query-options-erasure(two new untyped probe bags, now typedEngineQueryOptions) andcheck:test-source-alias(the pins reach AppPlugin's lazy@objectstack/metadataimport; now warmed at module top) fixed and exit 0;check:skill-examplesandcheck-plugin-teardown-shape --self-testrefused for a missing prerequisite (spec declarations, a commit outside the shallow clone), both exit 0 once met.check:dual-build-cjs-loads: NOT MEASURED, reason: it needs every package's dist/ (a fullpnpm build); CI's Lint & Repo Gates runs it.3c36f9c6e(after mergingorigin/mainonce), command as CI: build the closure, thenpnpm --filter @objectstack/dogfood testunder the lock (no shard, no path):VERDICT command-exit 1, Test Files 1 failed | 463 passed | 3 skipped (467), Tests 6 failed | 3921 passed | 42 skipped (3969). The one red file,test/rest-route-generation-tombstones.test.ts, failed every case onTS7016: Could not find a declaration file for module '@objectstack/spec/api': my post-merge rebuild usedOS_SKIP_DTS=1, sopackages/spec/distheld no declarations for its tsc fixture. Afterpnpm --filter @objectstack/spec buildwith declarations, that file: Test Files 1 passed, Tests 10 passed. Nothing in the diff reaches it.packages/runtime(this file,seed-replay-per-organization-identity,seed-loader-null-organization-holder-real-driver,app-plugin.seed-landed): Test Files 4 passed, Tests 29 passed.Acceptance notes
SEED_WRITE_OPTIONS, stamps nothing and has no organization; untouched.sys_memberdeclares no validity columns today, so "member" is "holds a row in the organization".sys_userandsys_organization) keep the filtered probe on an organization's load; it is refused and now said once. No scope rule is invented for them.singleboot, a publish and a hot reload probe references unscoped, as before; only their write context gains the sole organization.Verification
Round 1 (review fixes): two commits on top of
3c36f9c6e, pushed head752448bfb. Round 0 head:3c36f9c6e(merge oforigin/mainat6e1ab9b2..its tip, three commits, none on the seed loader or the replayer; no open PR touchesseed-loader.ts). Measured before the change onmain6e1ab9b; shape noteserver-seat-7/r2026-11-13/out/33207-33206-proposed-shape.md, accepted by the seat (claim amendments 6871524022 on #33207 and 6871533047 on #33206).🤖 Generated with Claude Code
https://claude.ai/code/session_01L9qibzSAkofrHDU1U8yFeA
Generated by Claude Code