Skip to content

fix(spec): one reader decides whether an external object's owner is provable, and record sharing and lint both read it - #27794

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-c3dfhl
Oct 30, 2026
Merged

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread

Fixes #27661

Clause-②: yes
Metadata-protocol: none

Fable review owed. This PR touches packages/spec/src/** and carries Clause-②: yes; it was developed on Opus while the Fable quota is out, and it does not land until a CONTRACT_REVIEW_TIER contract review record with a PASS verdict is on its current head.

维护者速读

What changes

  • packages/spec/src/data/injected-system-column-provenance.ts gains two exports on @objectstack/spec/data:
    • externalColumnMapTargets(def, field): does external.columnMap (remote column to local field) map some remote column onto field? Tolerant of un-parsed records; a non-object external or columnMap answers false.
    • isOwnerAnchorUnprovable(def): true exactly when resolveInjectedColumnProvenance(def, 'owner_id') === 'injected-unprovisioned' and externalColumnMapTargets(def, 'owner_id') is false. The one reader of "is an owner provable from declared metadata".
    • resolveInjectedColumnProvenance is unchanged, on purpose: a columnMap entry does not change who wrote the owner_id field, and lint's security-external-owner-unmapped needs that distinction (on a plugin driver the map is not applied, so only a declared field lets a member write). Folding the arm into the provenance verdict would have dropped that rule's mapped-but-undeclared findings. The new function's doc says so.
    • The convergence map's plugin-sharing lowers __readScope own/unit to an owner_id predicate on federated objects, where owner_id is a phantom column #7858 row now reads the head: plugin-sharing converges on isOwnerAnchorUnprovable (provenance plus the columnMap arm), and lint reads the same function.
  • packages/plugins/plugin-sharing/src/federated-phantom-anchors.ts: hasPhantomOwnerAnchor(schema) returns isOwnerAnchorUnprovable(schema). The module's own readFieldDef, equalsShippedDef, isFederatedObject and mapsRemoteColumnOntoOwner copies are deleted (none was exported from the package entry; git grep finds no other importer), and the module doc's convergence paragraph is rewritten. sharing-service.ts is untouched.
  • packages/lint/src/validate-external-owner-sharing.ts: hasUnprovableOwnerAnchor(obj) returns isOwnerAnchorUnprovable(obj); the private columnMapTargets is replaced by externalColumnMapTargets in externalOwnershipGap. Messages and hints are untouched.
  • packages/plugins/plugin-sharing/vitest.config.ts: the metadata-core alias comment no longer says the guard compares against OWNER_FIELD_DEF (only the scoping test's fixture imports it now).
  • packages/spec/api-surface/data.json and export-origins/data.json regenerated (two new names each).
  • Changeset .changeset/27661-owner-anchor-one-reader.md: patch on @objectstack/spec, @objectstack/plugin-sharing, @objectstack/lint.

Tests

On the final head e402956d (fe45a398 plus the merge of origin/main 79d78b4e), heavy runs under scripts/pm/os-verify-lock.sh:

  • pnpm --filter @objectstack/spec test: Test Files 754 passed (754), Tests 22410 passed, 1 todo (22411); exit 0. Taken on fe45a398; the merge brought no change to packages/spec/src/data/.
  • pnpm --filter @objectstack/spec exec vitest run src/data/injected-system-column-provenance.test.ts: 10 passed, 4 of them new (the unprovable verdict in both document stages and both fields shapes; the columnMap arm with provenance staying injected-unprovisioned; declared, local and owner-less tiers; columnMap read as values only, tolerant of bad shapes).
  • packages/plugins/plugin-sharing vitest run: Test Files 58 passed (58), Tests 1290 passed (1290), on e402956d. The federated phantom-scoping suites and external-owner-anchor-sql-read.test.ts are unchanged and green.
  • packages/lint vitest run: Test Files 178 passed (178), Tests 7623 passed (7623), on e402956d. validate-external-owner-sharing.test.ts is unchanged and green, so the rule's findings and hints keep their bytes.
  • pnpm --filter @objectstack/metadata-core test (the producer's provenance matrix, through the re-export): Test Files 21 passed (21), Tests 873 passed (873).
  • pnpm --filter @objectstack/spec typecheck, pnpm --filter @objectstack/lint typecheck, pnpm --filter @objectstack/plugin-sharing typecheck: exit 0.
Ablation: both consumers really read the spec reader

Committed head first; the mutation deletes the columnMap guard line from isOwnerAnchorUnprovable in packages/spec/src/data/injected-system-column-provenance.ts (grep -c of the guard line: 0 mutated, 1 restored, as at HEAD), then pnpm --filter @objectstack/spec build, because plugin-sharing and lint resolve @objectstack/spec/data through dist/.

  • Mutant: plugin-sharing external-owner-anchor-sql-read.test.ts + federated-phantom-owner-scoping.test.ts 3 failed of 55 (the columnMap to owner_id arms: "own rows only"); lint validate-external-owner-sharing.test.ts + validate-security-posture.test.ts 1 failed of 202 ("keeps the private recommendation where the owner is provable").
  • Restored with git checkout HEAD, git diff HEAD empty, spec rebuilt: 55 of 55 and 202 of 202 pass.

Acceptance notes

  • Edge move (a), unreachable at runtime. A hand-built federated document with no owner_id key at all, on an ownership: 'user' object, used to answer false in plugin-sharing (its copy needed the key present) and now answers true (the spec treats a pre-injection document as carrying the injected anchor, as lint already did). Every hasPhantomOwnerAnchor caller in sharing-service.ts either checks hasOwnerField(schema) first (buildReadFilter, buildWriteFilter, inertGrantReason) or is the verdict-free write-deny diagnostic, and the registry always injects owner_id on such an object, so no registered schema reaches it.
  • Edge move (b), fails closed both ways. An external object whose tier withholds owner_id (ownership: 'none' | 'org' | 'business_unit', systemFields: false) but which declares a field byte-identical to OWNER_FIELD_DEF used to read as phantom in plugin-sharing (deny-all below org) and now reads as the author's field (the spec's existing rule: with no injected definition, any declaration is the author's), so ordinary owner scoping applies. On a remote that lacks the column that predicate matches no row. Agreed with the epic PM before the edit.
  • Non-object external values (rejected by Zod, so only on un-parsed lint input): lint's hasUnprovableOwnerAnchor used to answer false for them and now follows the spec's external != null storage predicate, as plugin-sharing already did.
  • packages/qa/dogfood/test/federated-phantom-share-grant.dogfood.test.ts L251-L256 still describes hasPhantomOwnerAnchor as hand-rolled. It is a historical test comment outside this card's file surface; noted, not filed (carrier: none).
  • Serial constraint from the claim: plugin-sharing: the #8418 phantom-anchor write-deny diagnostic goes silent on a federated object that maps owner_id through external.columnMap over a plugin driver that ignores the map (epic #27261) #27662 edits plugin-sharing's write-deny diagnostic in sharing-service.ts; this PR does not touch that file, so the two do not overlap.

Verification

Local gates on e402956d, every exit code written to a file before any pipe. node scripts/pm/dispatch-gates.mjs --commands derived 86 families (the same 86 before and after the merge), every one was run after a full pnpm build:

  • 86 exit 0, among them spec check:api-surface ("public API surface + factory signatures unchanged"), check:export-origins ("5651 exports across 19 entry points resolve exactly as recorded"), check:docs ("232 generated files in sync"), check:authorable-surface, check:llms-txt, check:generated, check:cross-package-test-inputs, check:test-source-alias, check:type-check-debt, check:dual-build-cjs-loads, check:lean-entry-closure, check:i18n, check:doc-authoring, check:issue-citations, check:nul-bytes, check-adr-0087-registration --base origin/main, check-empty-changeset --base origin/main, check-changeset-no-major --base origin/main and the --self-test batteries.
  • dispatch-gates --ran: "86 derived, 86 run, 0 NOT-MEASURED, 0 UNRUN".
  • pnpm --filter @objectstack/spec check:generated after the merge and a spec rebuild: all 14 artifacts up to date. Before the merge it found api-surface/ and export-origins/ stale (the two new names), regenerated with gen:api-surface and gen:export-origins.
  • origin/main merged once, as e402956d, via scripts/pm/os-regen-merge.sh (main had not moved either regenerated artifact).
  • The full battery (pnpm lint, Test Core shards, the UI jobs) is CI's and was not re-run locally. No dogfood battery: no ! in the title and no (narrowing) arm.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (The new functions are document derivations beside the existing provenance derivation, tolerant of un-parsed input like the rest of the module.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint is CI's.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the repo's own Chinese reader section).
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…rovable

isOwnerAnchorUnprovable (and its columnMap reader externalColumnMapTargets)
join the injected-column provenance module; plugin-sharing's
hasPhantomOwnerAnchor and lint's hasUnprovableOwnerAnchor read it instead of
each restating the external.columnMap arm. resolveInjectedColumnProvenance is
unchanged, so lint's write rule keeps its mapped-but-undeclared findings.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2eCEdjSZBk7DGrPPFg
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/lint, @objectstack/plugin-sharing, @objectstack/spec, touching 12 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/plugins/plugin-sharing/vitest.config.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

32 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json e5cce0961ee9fb8c7962d55008943020d3a93cdb.

⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/plugins/plugin-sharing/vitest.config.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 58 of 210 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 — 167 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 e5cce0961ee9fb8c7962d55008943020d3a93cdb → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json e5cce0961ee9fb8c7962d55008943020d3a93cdb

⚠️ 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 e5cce0961ee9fb8c7962d55008943020d3a93cdb → 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

epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 rate_limit, 2026-10-29T23:29Z) · head e402956

The seat's at-tier review call was refused by the model quota, and per contract-review.md the tier is not lowered. Owed on this head: one same-shape CONTRACT_REVIEW_TIER record (faces: published schema packages/spec/src/data/injected-system-column-provenance.ts, Clause-②: yes), and the maintainer's independent full Fable review of the spec change. One record with a genuine ④ can serve both, as on #27715. Card #27661, os-dev-report 6510961760. The two edge moves are named in the PR's acceptance notes.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head e402956dd6fc25b37b1335be9d8f95f456144668 · 2026-10-29T23:33Z


Generated by Claude Code

…agnostic, read from the spec

The write-deny diagnostic that landed on main reads isFederatedObject from
federated-phantom-anchors.ts; it now answers the spec's
platformProvisionsStorage instead of restating the external check.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2eCEdjSZBk7DGrPPFg
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Head moved · 2026-10-29T23:36Z

For the contract review claimed on e402956d (6511029743): the head is now db96a59d. The delta is a merge of origin/main (f375a431) plus one restored export.

  • Cause: fix(plugin-sharing): a refused write on a federated object whose mapped owner column the driver did not serve is logged again #27760 (64599ec5) landed after this branch's last merge. It imports isFederatedObject from federated-phantom-anchors.ts into sharing-service.ts, and this diff had deleted that export. Every red job on e402956d came from that one build failure (@objectstack/plugin-sharing#build).
  • Fix: isFederatedObject stays as a one-line read of the spec predicate, !platformProvisionsStorage(schema). It gives the same answer for every input; sharing-service.ts is untouched.
  • Correction to the PR body: the line saying the isFederatedObject copies are deleted is stale. The other copies are gone, and this export remains as that one-line delegate.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e402956dd6fc25b37b1335be9d8f95f456144668
Local-runs: none
Scope: packages/spec/src/data/injected-system-column-provenance.ts + packages/plugins/plugin-sharing/src/federated-phantom-anchors.ts, packages/plugins/plugin-sharing/src/sharing-service.ts (read at the head and at the PR base 8f25cee2a49ff3feeb0dd6b0e381cdb36017e71e), packages/lint/src/validate-external-owner-sharing.ts, packages/lint/src/validate-security-posture.ts, packages/spec/src/data/object.zod.ts (external.columnMap), packages/spec/src/system/constants/system-names.ts, packages/spec/src/data/injected-system-column-provenance.test.ts, packages/plugins/plugin-sharing/src/index.ts. The rest of the diff (changeset, vitest.config.ts comment, the two regenerated spec registers) is the lane seat's ACCEPT at TIER_DEFAULT.

Inputs: card #27661 (body, comments 6509266755 and 6510961760), PR #27794 (body, 8-file list, net diff from the files endpoint), the 54 check-runs on the head, and the live tree at the head, at the PR base and at the merge-base 79d78b4ef761d1974ccfc56fb5c133b74d530bb9. Nothing was built, run or re-run; the merge-base and base check-runs were read from the API for the signature comparison. Reviewer: the reviewer tier.

① Derived judgments

  1. @objectstack/spec/data gains two exports, externalColumnMapTargets(def, field) and isOwnerAnchorUnprovable(def); packages/spec/api-surface/data.json and export-origins/data.json record exactly those two names and nothing else moves. resolveInjectedColumnProvenance, InjectedColumnProvenance, platformProvisionsStorage, unprovisionedInjectedColumns and isInjectedColumnDefinition are byte-identical, so no existing export changes its answer. Additive public-surface widening: RIGHT.
  2. Accept set of the published spec: no Zod schema is touched. external.columnMap stays z.record(z.string(), z.string()), described on main as "Remote column name → local field name" (object.zod.ts:1391); the new reader tests Object.values(columnMap).includes(field), i.e. the local side, and the test pins that a KEY named owner_id does not count. Direction read correctly: RIGHT. Metadata-protocol: none: RIGHT (nothing an author can write changes).
  3. packages/plugins/plugin-sharing/src/federated-phantom-anchors.ts loses four module symbols. readFieldDef, equalsShippedDef, mapsRemoteColumnOntoOwner were private: RIGHT. isFederatedObject was exported from the module, and on the PR's base 8f25cee2 it has an importer the diff's "no other importer" claim misses: sharing-service.ts:39 imports it and :1029 reads it (notePhantomAnchorWriteDeny's unserved-owner branch, plugin-sharing: the #8418 phantom-anchor write-deny diagnostic goes silent on a federated object that maps owner_id through external.columnMap over a plugin driver that ignores the map (epic #27261) #27662 / PR fix(plugin-sharing): a refused write on a federated object whose mapped owner column the driver did not serve is logged again #27760, commit 64599ec5, landed 22:54Z, after the branch's last merge of origin/main at 79d78b4e). The PR's three-dot diff is clean only because the branch predates that commit. Deleting the export is WRONG against the tree the PR lands on (④ must-fix 1). The symbol was never on the package entry (src/index.ts, head and base), so plugin-sharing's PUBLISHED surface is unchanged: that half RIGHT.
  4. Behaviour of hasPhantomOwnerAnchor on registered schemas is unchanged; it moves on two hand-built edges, (a) an external ownership: 'user' document with no owner_id key answers true (was false), (b) an external object on a tier that withholds owner_id but declares a field byte-identical to OWNER_FIELD_DEF answers false (was true). Both are derived correctly from the spec's existing provenance rule and both stay closed (③). RIGHT, and named by the dev.
  5. Lint: hasUnprovableOwnerAnchor now equals isOwnerAnchorUnprovable; the only divergence from the old body is a non-object external value (string, array), which Zod refuses and which externalOwnershipGap still guards on its own first line. externalOwnershipGap.mapped now reads externalColumnMapTargets(obj, column), the same predicate as the deleted columnMapTargets after the object guard already passed. Findings and hints on parsed input are byte-identical: RIGHT.
  6. The convergence map's plugin-sharing lowers __readScope own/unit to an owner_id predicate on federated objects, where owner_id is a phantom column #7858 row now says plugin-sharing converges on isOwnerAnchorUnprovable and lint reads the same function. On the head tree that is true: RIGHT. But the module header still names isFederatedObject copies converging on platformProvisionsStorage "when next touched" (line 262), and this PR touched plugin-sharing's copy by deleting it without converging the consumer main gained: see ④.

② Semver level

Changeset .changeset/27661-owner-anchor-one-reader.md: patch on @objectstack/spec, @objectstack/plugin-sharing, @objectstack/lint. AGENTS.md's rule: the level follows the Metadata-protocol kind alone; none takes patch, and a non-protocol widening ships patch. Two new exports and no authorable change is exactly that case: RIGHT. Clause-②: yes with no arm, line-initial in both the PR body and the changeset body; a bare yes is the allowed form for a public-surface widening, and the breaking clause (migration, ADR-0087 marker) is not triggered because nothing published is removed: RIGHT. Metadata-protocol: none in both: RIGHT. Check Changeset and the queue guard are green on the head. The changeset prose states the two edge moves and the resolveInjectedColumnProvenance non-move truthfully. Clause-②: line judged: yes, correct.

③ Boundary flags

  • open_questions: []: none to answer.
  • Deviation 1 (report line order): procedural, no bearing on the tree.
  • Deviation 2 (two --no-verify wip commits under fe45a398, not squashed because the branch is not claude/issue-*): the queue squash-merges, so the landing commit is the one with the trailer pair; accepted. Note that the squash would land the ④ must-fix 1 break, so the squash is not a mitigation here.
  • Deviation 3 (packages/plugins/plugin-sharing/vitest.config.ts outside the claimed surface): comment-only, describes the alias the deleted comparison used; accepted as lane remainder.
  • Deviation 4 (PR subscription): not a tree matter.
  • Edge move (a), answered: every hasPhantomOwnerAnchor caller that composes a filter checks hasOwnerField(schema) first (buildReadFilter :673, buildWriteFilter :813, inertGrantReason :1660 and :1663), notePhantomAnchorWriteDeny is a latched diagnostic, and the registry injects owner_id on every ownership: 'user' object, so no registered schema reaches the moved reading. The spec and lint already read a pre-injection document that way; plugin-sharing joining them is the card's point.
  • Edge move (b), answered with one nuance: the declared field IS the author's declaration, so 'author' is the right reading and it aligns plugin-sharing with lint, which already produced no finding for it. The PR's "matches no row" holds on SQLite; the plugin-sharing module header itself records that Postgres and MySQL raise column does not exist for the same predicate. Still closed (an error, not exposure), but the sentence should say so; carried as ④ should-fix 2.
  • Non-object external on un-parsed lint input, answered: Zod-rejected; externalOwnershipGap guards it independently; the only reachable site is the security-owd-unset hint arm (validate-security-posture.ts:843), on invalid input. Accepted.
  • Serial-constraint statement in the PR ("plugin-sharing: the #8418 phantom-anchor write-deny diagnostic goes silent on a federated object that maps owner_id through external.columnMap over a plugin driver that ignores the map (epic #27261) #27662 edits sharing-service.ts; this PR does not touch that file, so the two do not overlap"): WRONG on the base. plugin-sharing: the #8418 phantom-anchor write-deny diagnostic goes silent on a federated object that maps owner_id through external.columnMap over a plugin driver that ignores the map (epic #27261) #27662's PR fix(plugin-sharing): a refused write on a federated object whose mapped owner column the driver did not serve is logged again #27760 explicitly reused "the already-exported isFederatedObject" from federated-phantom-anchors.ts (its own body says so) to avoid adding a fourth columnMap reader; this PR deletes that export. Escalated to ④ must-fix 1.
  • Dogfood test comment (federated-phantom-share-grant.dogfood.test.ts L251-L256) and the plugin-security tenant twin: carrier: none, out of this card's surface; noted.
  • Gate verdicts on the head (the check-runs ARE the verdicts). Green: Lint & Repo Gates and every Lint lane, Check Changeset, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Check PR Size, Flag docs affected, Dogfood Regression Gate, Test UI, Type Check · source gates. Red: Build Core, Build UI Deps, Live E2E UI, Build & E2E UI, Test Core 1/8 to 8/8 and the Test Core aggregate, Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger and the TypeScript Type Check aggregate; Type Check UI cancelled behind the same step. Signature, read from the job logs: failed step "Build packages" / "Build workspace packages", first error line src/sharing-service.ts(39,33): error TS2305: Module './federated-phantom-anchors.js' has no exported member 'isFederatedObject', task @objectstack/plugin-sharing#build, on the CI merge commit b7cd3ea3 (head into base 8f25cee2). The merge-base 79d78b4e has 137 check-runs with no non-success conclusion, so this red is the PR's own, not an inherited main signature; the same-signature exception does not apply. GitHub reports the PR mergeable because the conflict is semantic, not textual: a squash-merge would land a red main.
  • The dev's local gates ("86 derived, 86 exit 0") were run on the head tree, which lacks fix(plugin-sharing): a refused write on a federated object whose mapped owner column the driver did not serve is logged again #27760; true of that tree, not of the tree that lands.
  • PR author is objectstack-fleet[bot]; no extra author note owed.

④ Design judgment

packages/spec/src/data/injected-system-column-provenance.ts, judged against the #7865 ruling (direction B: the registry keeps injecting, consumers converge on the spec's marker as they are touched), the #8116 ruling (the derivation lives in the spec so lint can reach it), the card's three task lines, and its first consumers on the live tree.

  • externalColumnMapTargets: reads the document only, tolerant of un-parsed input, values side of a remote-to-local map, exactly the direction object.zod.ts declares. Holds. The test pins key-vs-value and six bad shapes.
  • isOwnerAnchorUnprovable: columnMap arm first, then resolveInjectedColumnProvenance(def, 'owner_id') === 'injected-unprovisioned'. Against the old plugin-sharing predicate (isFederated && !mapped && equalsShippedDef(readFieldDef(owner_id), OWNER_FIELD_DEF)) it is equal on every registered schema: the provenance verdict is 'injected-unprovisioned' exactly when the tier injects owner_id, the document either lacks the key or carries the platform's definition, and external != null. The two hand-built differences are ① item 4, both closed. Against lint it is identical on parsed input. Holds; the four new spec cases cover both document stages, both fields shapes, the columnMap arm with provenance staying 'injected-unprovisioned', the declared/local/owner-less tiers and the tolerant reads. Not re-run here; the spec test file is unchanged by the merge and the red gates never reached a test step.
  • resolveInjectedColumnProvenance left unchanged: judged right, not merely accepted. Lint's undeclaredOwnershipColumn depends on 'injected-unprovisioned' surviving a columnMap entry to emit the mapped-but-undeclared finding and its mapped wording (security-external-owner-unmapped); folding the arm into provenance would have erased that finding. The card allowed either shape; the doc on the new function records why this one.
  • The plugin-sharing lowers __readScope own/unit to an owner_id predicate on federated objects, where owner_id is a phantom column #7858 convergence row reads the head's behaviour: holds for the head tree.
  • Tried to break it: a columnMap onto owner_id on a tier that withholds owner_id answers false (as both old readers did); fields as an array with a bare string element is skipped by readDeclaredFieldDef and the injection reading wins, the fail direction the module documents; a local object with a stray external: null still reads as provisioned. Nothing moved a verdict on a registered schema.

Must-fix 1 (FAIL). The head does not build against its base. This module's own doc (line 262) names platformProvisionsStorage as the ONE spelling of "the schema is the remote's" that isFederatedObject copies converge on when touched, and the PR touched plugin-sharing's copy by deleting it. But the consumer main gained in the meantime, sharing-service.ts:1029 (notePhantomAnchorWriteDeny's UNSERVED_OWNER_WRITE_DENY_NOTICE branch from the card's sibling #27662, PR #27760, commit 64599ec5), still imports isFederatedObject from ./federated-phantom-anchors.js. Every Build, Test Core and Type Check gate on the head is red with that one signature (③), the merge-base is fully green, and the PR is textually mergeable, so landing it as is puts main red. Fix, in the direction the hits file prescribes: sharing-service.ts reads !platformProvisionsStorage(schema) from @objectstack/spec/data in that branch (or, if the lane prefers a smaller diff, federated-phantom-anchors.ts keeps a one-line exported isFederatedObject that returns !platformProvisionsStorage(schema)), the branch re-merges origin/main, the #27662 block (6 cases in federated-phantom-owner-scoping.test.ts on main) runs green on the merged tree, and the PR's serial-constraint sentence is rewritten to say the two overlap and how. New head, new record.

Should-fix 1. isInjectedColumnDefinition's doc (line 233) still lists plugin-sharing among the hand-rolled equalsShippedDef copies awaiting convergence; after this PR only plugin-security's remains. One-line doc edit in the hits file, same commit as must-fix 1.

Should-fix 2. isOwnerAnchorUnprovable's fail-direction paragraph says ordinary owner scoping "on a column the remote lacks also matches no row". That is the SQLite reading; the plugin-sharing module header this row converges records that Postgres and MySQL raise. Say "matches no row or raises; neither widens" so the spec's one reader does not promise a dialect-specific outcome.

Implemented-by: claude/project-thread-c3dfhl
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head db96a59d16c7e63634b621d07e257c9aaf20d719 · 2026-10-29T23:44Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
Heads-up for the claim on db96a59d (6511206386) · 2026-10-29T23:45Z

One more commit is coming: the two doc-only should-fixes from 6511199047 (the isInjectedColumnDefinition convergence list, and "matches no row or raises; neither widens"). The serial-constraint sentence about #27760 will go up as a PR comment. The landing record has to sit on that head, so read db96a59d as the merge-plus-restore delta and stamp the record on the head after it. I'll name that head here once it's pushed.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: db96a59d16c7e63634b621d07e257c9aaf20d719
Local-runs: none
Scope: packages/spec/src/data/injected-system-column-provenance.ts + packages/plugins/plugin-sharing/src/federated-phantom-anchors.ts (at the head, at the previous head e402956d and at the merge-base 21c4bdd8), packages/plugins/plugin-sharing/src/sharing-service.ts (:39, :1019-1041), packages/plugins/plugin-sharing/src/federated-phantom-owner-scoping.test.ts (the #27662 block, :860-936), packages/plugins/plugin-sharing/src/index.ts, packages/lint/src/validate-external-owner-sharing.ts, packages/spec/src/data/injected-system-column-provenance.test.ts, packages/spec/api-surface/data.json, packages/objectql/src/federated-object.ts (read for the convergence-map doc only). The rest of the diff (changeset, vitest.config.ts comment, the two regenerated spec registers) is the lane seat's ACCEPT at TIER_DEFAULT.

Delta review on record 6511199047 (head e402956dd6fc25b37b1335be9d8f95f456144668, FAIL: one must-fix, two should-fixes). Inputs: card #27661 (body, comments 6509266755 and 6510961760), PR #27794 (body as stored at 23:45:45Z, 8-file list, net diff from the files endpoint), its comments 6511082299 (the lane's account of the move), 6511199047 (the previous record) and 6511218188 (the lane's heads-up), the 47 check-runs on the head read three times (23:45Z, 23:48Z, 23:50Z), and the live tree at the head, at e402956d, at the merge-base 21c4bdd8 and at origin/main 2a0b37bb. Nothing was built, run or re-run. Reviewer: the reviewer tier.

What moved, separated: git log e402956d..db96a59d is 14 commits, 12 of them origin/main carry-over (6d9419bd..21c4bdd8) brought in by the merge f375a431, plus the merge itself and ONE PR commit, db96a59d. The merge is clean: git diff f375a431^1 f375a431 restricted to the PR's 8 files is empty, so no conflict was resolved by hand and the merge changed nothing the PR authored; of the 12 main commits only 64599ec5 (#27760) touches a path this review reads (sharing-service.ts, federated-phantom-owner-scoping.test.ts, plugin-sharing index.ts, its changeset), and none touches the 8 PR files. The PR's own delta is one file, federated-phantom-anchors.ts, +11/-1: the import line gains platformProvisionsStorage, and export function isFederatedObject(schema: unknown): boolean { return !platformProvisionsStorage(schema); } is restored with a four-line doc. The 8-file list is unchanged; the net diff on the other 7 files is byte-identical to the one record 6511199047 judged. origin/main has moved 5 commits past the merge-base (e5cce096..2a0b37bb); none touches packages/spec/src/data, packages/plugins/plugin-sharing, or the lint rule, so the judgments below hold against today's main too.

① Derived judgments

Items 1, 2, 4, 5 of record 6511199047 stand as judged there: the move touches none of the files they read (the hits file, the lint rule, the test file and the two spec registers are byte-identical between e402956d and db96a59d).

3, re-judged. federated-phantom-anchors.ts now loses THREE module symbols, not four: readFieldDef, equalsShippedDef and mapsRemoteColumnOntoOwner, all private on main: RIGHT. isFederatedObject stays exported, and its one importer on the merged tree, sharing-service.ts:39 read at :1029 (notePhantomAnchorWriteDeny's UNSERVED_OWNER_WRITE_DENY_NOTICE branch from #27662), resolves. git grep isFederatedObject at the head finds no other importer of plugin-sharing's copy (the objectql and plugin-security hits are their own modules' functions). The symbol is still not on the package entry (src/index.ts at the head exports PHANTOM_ANCHOR_WRITE_DENY_NOTICE and, since #27760, UNSERVED_OWNER_WRITE_DENY_NOTICE, nothing from federated-phantom-anchors.ts), so plugin-sharing's PUBLISHED surface is unchanged by the whole PR: RIGHT.

3a, new. The restored isFederatedObject answers !platformProvisionsStorage(schema) in place of (schema as ...)?.external != null. Checked for every input class, not taken from the lane's "same answer for every input": platformProvisionsStorage returns true for null, undefined, arrays and every non-object primitive or function, and external == null for a plain object; the old body reads ?.external on the same inputs and gets undefined (hence false) for every one of those, and external != null for a plain object. The two are equal on every value, so sharing-service.ts:1029 and the six #27662 cases keep their verdicts by construction, and the fixtures those cases use (plain objects with or without external) exercise only the branch where the two bodies are literally the same comparison. RIGHT.

6, re-judged. The convergence map's #7858 row (hits file :329-337) reads the head's behaviour; it still does. The platformProvisionsStorage doc (:259-264) names the engine's isFederated and plugin-security's isFederatedObject as the two hand-rolled copies that converge "when next touched"; plugin-sharing's copy was never on that list and now reads the spec, in the direction that doc prescribes, so no doc line in the hits file is falsified by the move. RIGHT. (The engine's federated-object.ts:33 still restates external != null; it was not touched and is not this card's.)

② Semver level

Stands as judged in record 6511199047. The changeset, Clause-②: yes, Metadata-protocol: none and patch on the three packages are unchanged by the move; a restored module-internal export that was never published changes no level. Check Changeset and the Governed Surface Queue Guard are green on the head. Clause-②: line judged: yes, correct.

③ Boundary flags

  • open_questions: [] and deviations 1 to 4: stand as judged in record 6511199047. Deviation 2's note there (the squash would land the build break) no longer applies: the merged tree builds (below).
  • Edge moves (a) and (b) and the non-object external note: stand as judged there; the move does not touch hasPhantomOwnerAnchor, isOwnerAnchorUnprovable or the lint rule.
  • Must-fix 1 of record 6511199047, the tree half: DISCHARGED. The head re-merges origin/main past fix(plugin-sharing): a refused write on a federated object whose mapped owner column the driver did not serve is logged again #27760, keeps isFederatedObject exported as a one-line read of the spec predicate (the smaller of the two shapes the record offered), and sharing-service.ts:39 resolves against it. The check-runs confirm it: every job that was red on e402956d with the signature src/sharing-service.ts(39,33): error TS2305 ... has no exported member 'isFederatedObject' and has finished on db96a59d is now green: Build Core, Build UI Deps, Build & E2E UI, Live E2E UI, Type Check · consumer gates, Type Check · debt ledger and Test Core (8/8). None has turned red.
  • Must-fix 1, the prose half: NOT discharged in the PR body. As stored at 23:45:45Z the body still says the isFederatedObject copy is deleted with "no other importer", and its Acceptance note still says "plugin-sharing: the #8418 phantom-anchor write-deny diagnostic goes silent on a federated object that maps owner_id through external.columnMap over a plugin driver that ignores the map (epic #27261) #27662 edits sharing-service.ts; this PR does not touch that file, so the two do not overlap". Both are false on the merged tree: the two PRs share isFederatedObject, and fix(plugin-sharing): a refused write on a federated object whose mapped owner column the driver did not serve is logged again #27760 chose to import it precisely to avoid a fourth columnMap reader. The lane's comment 6511082299 states the correction and the overlap on the PR thread, and 6511218188 says the serial-constraint sentence will follow as a comment. The tree is what lands and the tree is right, so this does not hold the verdict; it is carried as should-fix 3 because the body is the account a maintainer reads first, and ⛔ a record that let a false "does not overlap" stand unremarked would be judging the body by the body.
  • Should-fix 1 and should-fix 2 of record 6511199047: STILL OPEN on this head. Hits file :233-234 still lists "plugin-security / plugin-sharing" as the hand-rolled equalsShippedDef copies awaiting convergence (plugin-sharing's is gone); :441-442 still says ordinary owner scoping on a column the remote lacks "also matches no row" (the SQLite reading; Postgres and MySQL raise, as the plugin-sharing module header :33-36 records). The lane's heads-up 6511218188 says both go in a further commit; they remain should-fixes, as before.
  • Gate verdicts on the head (47 check-runs; the conclusions ARE the verdicts). Green (28): Lint & Repo Gates and every Lint lane, Build Core, Build UI Deps, Build & E2E UI, Live E2E UI, Test UI, Test Core (8/8), Plan Test Core shards, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Check Changeset, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Check PR Size, Flag docs affected, Dogfood Regression Gate, Auto Label, filter. Skipped (10): opt-in and matrix placeholders, as on the previous head. PENDING, not green (9 at 23:50Z): Test Core (1/8) to (7/8), Type Check · workspace, Type Check UI; none has a conclusion and none is read as one here. The landing rule (every check green) is the queue guard's to read once they finish; this record's verdict is on the tree and does not stand in for them.
  • The dev's local gates ("86 derived, 86 exit 0") were run on e402956d; the lane reports no local run on db96a59d. The head's own check-runs are what this record reads.
  • PR author is objectstack-fleet[bot]; no extra author note owed.

④ Design judgment

packages/spec/src/data/injected-system-column-provenance.ts is byte-identical to the file record 6511199047 judged: externalColumnMapTargets, isOwnerAnchorUnprovable, the unchanged resolveInjectedColumnProvenance, the #7858 row and the break attempts stand as judged there. Judged afresh, the one thing the move adds, against the code it cites.

  • The restored isFederatedObject in federated-phantom-anchors.ts:96-98, judged against the hits file's own prescription (:259-264: platformProvisionsStorage is "ONE spelling of this schema is the remote's, exported so consumers stop re-spelling it") and against its first consumer, sharing-service.ts:1029. Holds: the delegate adds no second spelling, carries no columnMap reader and no identity comparison, so the module header's claim at :82-85 ("this module has converged: it holds no copy of the identity comparison and no columnMap reader of its own") stays true with the export present. Its doc names the spec function and the ruling it converges under, and claims nothing about behaviour the function does not have.
  • Tried to break it. getSchema?.(object) returning undefined (an object the engine does not know): isFederatedObject(undefined) is false under both bodies, so the diagnostic stays silent and checkEdit still answers deny from the owner == null arm. A registered local object with external: null stamped by a parse: external == null is true, so false under both bodies. A schema in the array fields shape with external: {}: true under both. hasPhantomOwnerAnchor is consulted FIRST at :1028, so a phantom anchor on a federated object still gets the phantom wording and the unserved-owner branch only sees federated objects whose owner is provable by declaration, exactly the class plugin-sharing: the #8418 phantom-anchor write-deny diagnostic goes silent on a federated object that maps owner_id through external.columnMap over a plugin driver that ignores the map (epic #27261) #27662 built it for. Nothing moved a verdict or a notice.
  • Against the plugin-sharing: the #8418 phantom-anchor write-deny diagnostic goes silent on a federated object that maps owner_id through external.columnMap over a plugin driver that ignores the map (epic #27261) #27662 block on the merged tree (federated-phantom-owner-scoping.test.ts:860-936, 6 cases): ext_plugin_mapped and ext_plugin_mapped_2 (mapped, map-ignoring driver) reach :1029 with row.owner_id === undefined and a plain external object, so isFederatedObject is true and the unserved notice fires once per object; ext_sql_mapped and ext_with_real_owner never reach owner == null on the own rows and reach it with the key PRESENT on s_nobody, so no notice; missing has no row and returns before the predicate. Each case decides on the same external != null fact under both bodies. Not run here; Test Core (8/8) is green and the seven other shards are pending, so the suite's own conclusion is theirs to render.

Must-fix: none. Record 6511199047's must-fix 1 is discharged on the tree: the head builds against its base and against today's origin/main, the restored export reads the spec in the direction the hits file prescribes, and the merge carried nothing by hand.

Should-fix 1 (carried from 6511199047, open). Hits file :233-234: isInjectedColumnDefinition's doc still lists plugin-sharing among the hand-rolled equalsShippedDef copies awaiting convergence; after this PR only plugin-security's remains.

Should-fix 2 (carried from 6511199047, open). Hits file :441-442: say "matches no row or raises; neither widens" in isOwnerAnchorUnprovable's fail-direction paragraph, so the spec's one reader does not promise the SQLite outcome for Postgres and MySQL.

Should-fix 3 (new). The PR body's two stale sentences: the "What changes" bullet that says isFederatedObject is deleted with no other importer, and the Acceptance note that says this PR and #27662 do not overlap. Rewrite them to say the export stays as a one-line read of platformProvisionsStorage because sharing-service.ts:1029 (#27662, PR #27760) imports it, the body's account of the serial constraint being the thing the previous record found wrong. Comment 6511082299 already carries the correction; the body should too.

Implemented-by: claude/project-thread-c3dfhl
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
ACCEPT · PR #27794 @ db96a59d16c7e63634b621d07e257c9aaf20d719 · 2026-10-29T23:58Z

维护者速读

席位意见:ACCEPT。契约复核 PASS 在案(分诊 6511315513,CONTRACT_REVIEW_TIER,就是当前 head,同一记录兼作 spec 改动的独立完整复核),无 must-fix。

席位核对

  1. Shape: +179/−109, 8 files. Two additive exports on @objectstack/spec/data (api-surface and export-origins record only those two names). Plugin-sharing and lint read them. resolveInjectedColumnProvenance is byte-identical.
  2. Premise measured: before this PR, hasPhantomOwnerAnchor and lint each held their own columnMap reader and identity comparison, and the spec helper had no columnMap arm. The record checks equality on every registered schema and names the two hand-built edges, both closed.
  3. Gate check-runs on db96a59d: 52 runs: 40 success, 12 skipped, 0 failed or pending. mergeable_state: clean. Local on the merged tree: plugin-sharing 1296/1296, including fix(plugin-sharing): a refused write on a federated object whose mapped owner column the driver did not serve is logged again #27760's 6 cases in federated-phantom-owner-scoping.test.ts.
  4. Accepted deviations and PR-body correction: the body's "What changes" bullet says isFederatedObject is deleted with no other importer, and its Acceptance note says this PR does not overlap plugin-sharing: the #8418 phantom-anchor write-deny diagnostic goes silent on a federated object that maps owner_id through external.columnMap over a plugin driver that ignores the map (epic #27261) #27662. Both are stale. isFederatedObject stays exported as a one-line read of platformProvisionsStorage because sharing-service.ts:1029 (plugin-sharing: the #8418 phantom-anchor write-deny diagnostic goes silent on a federated object that maps owner_id through external.columnMap over a plugin driver that ignores the map (epic #27261) #27662, PR fix(plugin-sharing): a refused write on a federated object whose mapped owner column the driver did not serve is logged again #27760) imports it. Head-move comment 6511082299 and record 6511315513 carry the correct account; the relay refuses a body patch on this PR.
  5. Out of scope, filed: the record's two doc-only should-fixes (the isInjectedColumnDefinition convergence list; "matches no row or raises; neither widens") ride spec: composed stacks refuse a datasource whose external.connector names no declared connector instance (composed counterpart of the #27742 check, #25211 shape) — epic #27261 #27803 (scope addition 6511327300). They were not pushed here, so this PR lands on its reviewed head.
  6. Landing: not governed (check-governed-merges --pr 27794: 288 lines ≤ 3000, 0 of 8 paths governed). A same-shape PASS stands on the current head, so needs:contract-review comes off. Landed by the seat through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 23:59
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 29, 2026 23:59
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 684f2f6 Oct 30, 2026
53 checks passed
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 protocol:data size/m tests tooling

Projects

None yet

2 participants