Skip to content

refactor(core,objectql,plugin-sharing): one PackageScopedNameRefusedError class for every package-scoped by-name refusal - #31876

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-mvgczn
Nov 9, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-mvgczn

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #31852

Clause-②: yes (widening) — @objectstack/core and the lean entry @objectstack/objectql/core gain PackageScopedNameRefusedError
Metadata-protocol: none

维护者速读

  • 改了什么:包作用域按名读取被拒时抛出的 PackageScopedNameRefusedError 原本在 objectql 和 plugin-sharing 各有一份,现在只在 @objectstack/core 声明一次,两个包原名转出同一个类;objectql 的精简入口 @objectstack/objectql/core 也导出它。导航翻译检查脚本遇到同名应用被拒时,改为报一条列出所有候选包的组合错误,不再抛出 409 中断。
  • 为什么改:两份同名类互不 instanceof,以后谁要捕获"包作用域名字冲突"就得猜用哪一份,猜错一半;精简入口的使用者只能按 code 和 status 匹配。
  • 风险与回滚:每个入口仍返回 409 RESOURCE_CONFLICT、同一句话,行为不变;plugin-sharing 抛出的错误多带一个值相同的 httpStatus 字段。回滚即还原本 PR。
  • 先看:packages/core/src/package-scoped-resolution.ts(类本体)、packages/plugins/plugin-sharing/src/package-scoped-declarations.ts(改为转出)、packages/cli/scripts/check-app-nav-i18n.mjs(readMergedApp)。
  • 两分钟验证:pnpm --filter @objectstack/plugin-sharing exec vitest run src/package-scoped-declarations.test.ts,预期 26 个全过,其中 3 个是本 PR 新增的跨包 instanceof 断言。
  • 需要你做的:无,席位落地。

What changes

  • @objectstack/core (package-scoped-resolution.ts): PackageScopedNameRefusedError is declared here once, beside PackageScopedRefusal and packageScopedRefusalMessage. It carries the union of the two old copies' fields: code = 'RESOURCE_CONFLICT', status = 409, httpStatus = 409 and refusal, and its message is the refusal's own sentence. Core's barrel already re-exports the module (export *), so no barrel edit.
  • @objectstack/objectql (package-scoped-read.ts): the local class is gone; the module re-exports core's binding under the same name, so the barrel, metadata-facade.ts and registry.ts keep their imports.
  • @objectstack/objectql/core (lean entry, core.ts): exports the class too, since the SchemaRegistry it exports throws it from getApp. Core is already in the entry's closure, so core-boundary.ratchet.test.ts stays green.
  • @objectstack/plugin-sharing (package-scoped-declarations.ts): the local class is gone; the module re-exports core's binding under the same name, so sharing-rule-service.ts and ownership-queue-service.ts keep their imports. Its refusals gain httpStatus: 409.
  • packages/cli/scripts/check-app-nav-i18n.mjs (N1): the app is read through a new pure readMergedApp(name, getApp, isRefusal). A PackageScopedNameRefusedError from registry.getApp(name) becomes a composition finding naming the app and every candidate, reported beside the gate's other composition errors (so the coverage verdict is not attempted). It never reads as "not registered", whose remedy is the opposite. Any other error is rethrown untouched. I chose "catch and report" over "read by the shell's package": a read naming the shell package would silently pick the shell's app and hide a real name clash from the gate.

Measured site census (delta ruling 1), git grep -n "PackageScopedNameRefusedError\|RESOURCE_CONFLICT.*refusal" on origin/main 074a014c: two declarations (objectql package-scoped-read.ts, plugin-sharing package-scoped-declarations.ts); throws in package-scoped-read.ts, sharing-rule-service.ts (2) and ownership-queue-service.ts (2); instanceof in metadata-facade.ts, sharing-rule-service.ts and ownership-queue-service.ts (2). No third copy on main. Open PRs read at their heads: E3 #31815 adds no site. plugin-email #31825 adds a separate class, EmailTemplateAmbiguousError (same 409 RESOURCE_CONFLICT envelope, different name, extra template and candidates fields), in plugin-email/src/template-loader.ts. It is left to that PR's seat, as ruled.

Changeset: .changeset/31852-one-package-scoped-refusal-class.md, patch for core, objectql and plugin-sharing. @objectstack/cli takes none: its files is dist, README.md and CHANGELOG.md, so scripts/ does not publish.

Tests

  • packages/core/src/package-scoped-resolution.test.ts: the class carries code, status, httpStatus, refusal and the refusal's sentence.
  • packages/objectql/src/package-scoped-e8-readers.test.ts: toBe identity of the class from package-scoped-read.js, the barrel and the lean ./core entry against core's; a getApp refusal is instanceof the lean entry's class.
  • packages/plugins/plugin-sharing/src/package-scoped-declarations.test.ts: toBe identity of plugin-sharing's and objectql's export against core's; a refusal from objectql's by-name reader (SchemaRegistry#getApp) is instanceof plugin-sharing's class, and a refusal from plugin-sharing's door (SharingRuleService#declaredRuleNamed) is instanceof objectql's class.
  • check-app-nav-i18n.mjs --self-test: six new cases for readMergedApp (refusal becomes a finding, names every candidate, never reads as unregistered; served and unregistered reads unchanged; a non-refusal error is rethrown, as a negative control; the boot read passes the class test).
  • Wire envelope (delta ruling 5) already pinned by existing tests, unchanged: objectql package-scoped-e8-readers.test.ts ("the refusal crosses a wire door as 409 RESOURCE_CONFLICT") and plugin-sharing package-scoped-declarations.test.ts ("refuses the sharing rule by name with 409 RESOURCE_CONFLICT naming both packages").

No existing test was edited or deleted.

Acceptance notes

Verification

Head 3bb5c3e6 (02e922a7 plus a merge of origin/main).

Red first, on origin/main 074a014 (tests added, fix not yet applied)
  • core package-scoped-resolution.test.ts: Tests 1 failed | 23 passed (24).
  • objectql package-scoped-e8-readers.test.ts: Tests 2 failed | 71 passed (73).
  • plugin-sharing package-scoped-declarations.test.ts (core and objectql built from main): Tests 3 failed | 23 passed (26).
Ablation (scripts/ablation-replace.mjs, both legs restored to HEAD by blob hash)
  • plugin-sharing: export { PackageScopedNameRefusedError }; replaced by a local class. mutation landed: anchor 1 -> 0; Tests 3 failed | 23 passed (26), all three new cross-package pins red; restored: blob == HEAD (339e4de5b597).
  • objectql: the same replacement in package-scoped-read.ts. mutation landed: anchor 1 -> 0; Tests 1 failed | 72 passed (73) (the identity pin); restored: blob == HEAD (45f07d0a11c9).
Builds, tests, typecheck, lint
  • turbo run build --filter='./packages/**' --concurrency=2 under the lock at 02e922a7: VERDICT command-exit 0, Tasks: 81 successful, 81 total.
  • At 02e922a7, one lock run: typecheck for core, objectql, plugin-sharing, then pnpm --filter @objectstack/core test (Test Files 139 passed, Tests 3207 passed), plugin-sharing (Test Files 72 passed, Tests 1496 passed), objectql (Test Files 580 passed, Tests 11936 passed): VERDICT command-exit 0.
  • At 3bb5c3e6 (after the merge; incoming main touched other files of core and objectql): rebuild of the three packages, their typecheck, and the three changed test files plus core-boundary.ratchet.test.ts: 24/24, 75/75, 26/26, VERDICT command-exit 0.
  • pnpm check:app-nav-i18n (self-test, then the live composition): exit 0, OK (13 contributor(s), 11 locale(s), 2 app(s) — setup: 57 merged nav id(s), account: 13 merged nav id(s) …).
  • eslint --no-inline-config on packages/core/src, packages/objectql/src, packages/plugins/plugin-sharing/src and the script: exit 0.
Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 73 commands, each run with its exit code captured before any pipe, all exit 0 at 02e922a7. Reconciliation: 73 derived famil(ies) accounted for — 73 run, 0 NOT-MEASURED (a DERIVED zero — all 73 recorded an exit code and none of them is 3). Re-run at 3bb5c3e6: see the report comment on the card.


Generated by Claude Code

…kage-scoped by-name refusal

The class moves to @objectstack/core beside PackageScopedRefusal, carrying
the union of both copies' fields (code, status, httpStatus, refusal).
objectql and plugin-sharing re-export the same binding under the same name,
and the lean @objectstack/objectql/core entry exports it too, so one
instanceof matches a refusal from either package.

check-app-nav-i18n reports a refused app read as a composition finding that
names every candidate instead of letting the 409 escape the gate.

Claude-Session: https://claude.ai/code/session_01ADHkmQHXhv9SKTU1qQ9CNC
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m 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 Nov 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/core, @objectstack/objectql, @objectstack/plugin-sharing, touching 3 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/core.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

19 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 b6fa08e3a1deae066640e6842931e9dd62115b05.

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/core.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 231 client-bound route-ledger rows — the other 166 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 166: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 107 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 — 52 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 b6fa08e3a1deae066640e6842931e9dd62115b05 → packageMentionDocs.

Which tree this was computed on

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

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

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

domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 3bb5c3e6ed4fd294d1ebffaafbdce8afa5bab9f5
Local-runs: none

Inputs: card #31852 (body and the seat's delta), PR #31876 body and file list, and the net diff of 3bb5c3e6 against main (9 files, +235/−44).

① Derived judgments

  1. @objectstack/core gains one export, PackageScopedNameRefusedError (package-scoped-resolution.ts, reached through core's existing export *). Its fields are the union of the two old copies: code = 'RESOURCE_CONFLICT', status = 409, httpStatus = 409, refusal, and name and message as before. Right: one class at the lowest package every by-name door reads, beside the PackageScopedRefusal it carries.
  2. objectql and plugin-sharing keep the name: each module re-exports core's binding under the same name, so every existing import resolves, and the PR pins identity (toBe) and cross-package instanceof both ways. Right: nothing removed or renamed, so no narrowing arm is owed. plugin-sharing's refusal gains httpStatus: 409, a field with the value its status already had: additive.
  3. @objectstack/objectql/core (lean entry) gains the export: the SchemaRegistry it already exports throws this class from getApp. Core is already in the entry's closure; core-boundary.ratchet.test.ts stays green. Right.
  4. check-app-nav-i18n.mjs: a refused getApp becomes a composition finding naming every candidate; any other error is rethrown. A repo script, not published (@objectstack/cli ships dist only). Right: no contract surface.
  5. No spec, schema, protocol, wire or stored-row change: every door still answers 409 RESOURCE_CONFLICT with the same sentence, pinned by the existing envelope tests. Metadata-protocol: none is right.

② Semver level

Clause-②: yes (widening), naming the core and lean-entry exports, on the changeset and the PR body; patch on core, objectql and plugin-sharing. Right: a non-protocol widening ships patch (AGENTS.md Post-Task Checklist step 3), as #31464 and #31812 (PR #31846) did. No BREAKING banner is owed.

③ Boundary flags

Implemented-by: claude/project-thread-mvgczn
Reviewed-by: session_01ADHkmQKZzp4DLtqiCkZZRM

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM

维护者速读

  • 改了什么: "包作用域按名读取被拒"的错误类 PackageScopedNameRefusedError 以前在 objectql 和 plugin-sharing 各有一份,现在只在 @objectstack/core 定义一次,两个包原名转出同一个类;精简入口 @objectstack/objectql/core 也导出它。导航翻译检查脚本遇到同名应用被拒时,改报一条列出所有候选包的组合错误,不再直接 409 中断。
  • 为什么改: 两份同名类互相 instanceof 不成立,捕获方只能猜用哪份或按 code/status 匹配。
  • 风险与回滚: 每个入口仍回 409 RESOURCE_CONFLICT、同一句话;plugin-sharing 的错误多带一个值相同的 httpStatus。回滚即还原本 PR。
  • 先看: packages/core/src/package-scoped-resolution.ts 的类本体。
  • 需要你做的: 无,由席位落地。
  • 席位意见: ACCEPT。

席位核对 — ACCEPT · domain:server#2 · Head-sha 3bb5c3e6ed4fd294d1ebffaafbdce8afa5bab9f5 · 2026-11-09T09:14Z

  1. Shape: draft to main, Fixes #31852. The declaration is Clause-②: yes (widening), naming the core and lean-entry exports, with Metadata-protocol: none. Patch changesets for core, objectql and plugin-sharing; none for @objectstack/cli, whose scripts/ does not publish. 9 files, +235/−44.
  2. Contract review: TIER_DEFAULT, declaration face, PASS, on this head.
  3. Delta rulings, checked against the diff:
  4. Gates:
    • Dev: red first on main 074a014c (core 1, objectql 2, plugin-sharing 3); both ablations red and restored by blob hash; core 3207, objectql 11936, plugin-sharing 1496 tests pass; typecheck and eslint 0; check:app-nav-i18n exit 0; 73/73 derived gates, 0 NOT-MEASURED.
    • Check-runs on this head: 54 success, 11 skipped, 0 failed.
    • Governed surfaces: 0 of 9 paths on the register.
  5. Landing: pr_ready + automerge_enable.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 9, 2026 09:15
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 9, 2026 09:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 1a66fcf Nov 9, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-mvgczn branch November 9, 2026 09:39
rajeev-kumar-se pushed a commit to rajeev-kumar-se/objectstack that referenced this pull request Nov 10, 2026
…t binding, read from the spec predicate (objectstack-ai#31896)

<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9H71EcHQuc15PBmrPVnshdn)_
Fixes objectstack-ai#31849

Clause-②: no
Metadata-protocol: none

## 维护者速读

- 改了什么:“这个对象是不是联邦对象(ADR-0015 `external`)”在运行时只剩一个函数:core 的
`isFederatedObject`,它直接读 spec 的
`platformProvisionsStorage`;plugin-security、plugin-sharing 改为转出 core
的同一个函数,另外 7 处手写的 `external` 判断也改为调用它。
- 为什么改:同一个判断写了十来遍,将来“联邦”的含义一变(比如改读 `schemaMode`)就得逐处去找,漏一处就会出现两层答案不一致。
- 风险与回滚:行为不变:对所有对象记录以及 `null`、`undefined`、数组、原始值,新旧答案相同。回滚即撤本 PR。
- 先看:`packages/core/src/security/tenant-field.ts`(`isFederatedObject`
本体);`packages/plugins/plugin-security/src/federated-phantom-anchors.ts`(删掉的副本);`packages/core/src/security/tenant-field.test.ts`(新钉)。
- 两分钟验证:`git grep -n "external != null\|external == null" packages/core
packages/objectql packages/plugins` 预期在非测试源码里零命中。
- 需要你做的:无,席位落地。

## What changes

- `@objectstack/core` `isFederatedObject` (`security/tenant-field.ts`)
returns `!platformProvisionsStorage(schema)` from
`@objectstack/spec/data`, the spelling objectstack-ai#7865's convergence map names.
- `@objectstack/plugin-security` and `@objectstack/plugin-sharing`
`federated-phantom-anchors.ts` drop their own copy (security: `external
!= null`; sharing: its own `!platformProvisionsStorage` wrapper) and
`export { isFederatedObject }` from `@objectstack/core`. The export name
stays where it was, so every importer keeps resolving.
`@objectstack/objectql` keeps re-exporting the core binding, unchanged.
- The other runtime copies of the presence test, found by the measure
below, now ask the same binding:
- `packages/objectql/src/search-companion.ts` `isDdlManaged` (`external
== null`)
- `packages/cli/src/commands/migrate/unmapped-columns.ts` (the
"federated" outside reason)
- `packages/cli/src/utils/schema-migrate.ts` (managed-drift driver scan)
- `packages/cli/src/utils/schema-migration-plugins.ts` (coverage pass
`federated` count)
- `packages/services/service-analytics/src/plugin.ts` `isExternalObject`
(ADR-0062 D6 gate)
- `packages/triggers/trigger-schedule/src/time-relative-trigger.ts`
`organizationScopeIsInertFor`
- `packages/metadata-protocol/src/protocol-save-door-refusals.ts`
(datasource-bound `external` read; was `!== undefined && !== null`)
- Comment-only rewording where a docblock named the old expression as
the predicate: `objectql` `engine-driver-options.ts`,
`integrity/dangling-reference-audit.ts`; `plugin-security`
`security-plugin.ts`; `plugin-sharing` `sharing-service.ts`.
- One forced test edit:
`packages/objectql/src/federated-injected-column-readers.test.ts` gains
a READERS row `search-companion.ts#isDdlManaged :: isFederatedObject()`
(`not-a-read`), because the search companion now calls the seam by name.

## Why

Measure on `origin/main` 3f0e3ac (`git grep -n "external !=
null\|external == null\|external !==
undefined\|isFederatedObject\|platformProvisionsStorage"` under
`packages/`, non-test): besides the card's table, seven more source
sites restated the presence test (listed above). Seat ruling 1 converges
them too. Not converged, because they are a different question:
`service-datasource` `external-datasource-service.ts:1374` (`external
!== undefined || datasource !== 'default'`, a wider "routed off the
default datasource" test) and `datasource-admin-service.ts`
(`patch.external`, a patch field, not an object). `packages/spec` keeps
the one definition (no spec edit).

Input equivalence (seat ruling 2): `platformProvisionsStorage` answers
`true` (not federated) for `null`, `undefined`, arrays and any
non-object, and the old `?.external != null` answered `false` (not
federated) on each of those, so the two agree. The only inputs where
they differ are a function or an array that carries an own `external`
property; no producer hands one to these seams (all read registry or
overlay object records).

## Tests

- `packages/core/src/security/tenant-field.test.ts` (new):
`isFederatedObject(x) === !platformProvisionsStorage(x)` and the
expected verdict for `{}`, `{ external: {} }`, `{ external: null }`, `{
external: undefined }`, `null`, `undefined`, `[]`; plus barrel identity.
-
`packages/plugins/plugin-security/src/federated-object-binding.test.ts`
and
`packages/plugins/plugin-sharing/src/federated-object-binding.test.ts`
(new): the module export `toBe` core's binding.
- Why identity: a restored local copy answers the same as core on every
record, so no behavioural pin can see it (measured: the core ablation
below stays green). Identity is the only thing that changes.

<details><summary>Ablations (ruling 4), each restored with blob == HEAD
and empty diff</summary>

- plugin-security: delete the core import and restore `export function
isFederatedObject(schema) { return schema?.external != null }` →
`federated-object-binding.test.ts` RED (`expected [Function
isFederatedObject] to be [Function isFederatedObject] // Object.is
equality`), `federated-tenant-layer0.test.ts` stays green (behaviour
identical). Tests 1 failed | 8 passed.
- plugin-sharing: delete the core import and restore the old
`!platformProvisionsStorage` wrapper →
`federated-object-binding.test.ts` RED, same assertion; Tests 1 failed |
57 passed.
- core: revert the body to `?.external != null` → `tenant-field.test.ts`
stays GREEN (8/8): the agreement pin cannot see a revert, by design,
which is why the re-export layers carry the identity pin.
- Mutations landed through `scripts/ablation-replace.mjs` (anchor 1 → 0,
marker grep 1); tests import the module under test from `src`, so no
dist leg.
</details>

## Acceptance notes

- Test files still name `external != null` in prose or as a test variant
(e.g. `injected-system-columns-parity.test.ts`, the dangling-audit
federated phantom test); they describe what a mutation would look like,
not a predicate copy.
- `packages/spec/src/data/injected-system-column-provenance.ts:263`
still says plugin-security holds a hand-rolled copy; that doc is spec
prose and this card forbids `packages/spec/**` edits.

## Verification

Head `10eb82ed` (origin/main 1a66fcf merged in; the merge brought
objectstack-ai#31876, which edits plugin-sharing and core in other files).

- `git grep -n "external != null\|external == null" packages/core
packages/objectql packages/plugins` (non-test, CHANGELOG excluded): 0
hits. Under all of `packages/`, the only remaining hits are the
definition in
`packages/spec/src/data/injected-system-column-provenance.ts`.
- Build: `turbo run build --filter='./packages/**'
--filter='./examples/**'` under the verify lock, VERDICT command-exit 0.

<details><summary>Package suites and typecheck</summary>

At `10eb82ed` (after the main merge):
- core: typecheck + test, Test Files 141/141, Tests 3237/3237
- plugin-sharing: typecheck + test, Test Files 73/73, Tests 1497/1497
- plugin-security: typecheck + test, Test Files 252/252, Tests 6051
passed, 45 skipped
- objectql: typecheck + `federated-injected-column-readers`,
`federated-object`, `search-companion*`, `package-scoped*` (6 files,
212/212)
- cli, metadata-protocol, service-analytics, trigger-schedule:
typecheck, exit 0

At `73db2448` (before the merge; the code these suites exercise is
unchanged since):
- objectql full: Test Files 580/580, Tests 11935/11935
- service-analytics full: Test Files 214/214, Tests 5140 passed, 332
skipped
- trigger-schedule full: Test Files 12/12, Tests 230/230
- metadata-protocol full: Test Files 330 passed, 5 skipped; Tests 22626
passed, 29 skipped
- cli `--project unit`: 404/406 on the first run. The 2 failures were
`published-subpath-console.pin` and `published-subpath-hook-body.pin`,
which need `packages/cli/dist` and stopped with "packages/cli is not
built". After the full build they passed 2/2 (29 tests). The integration
tier is left to CI.
- Lint: root eslint `--no-inline-config` on the changed `.ts` files,
exit 0.
</details>

<details><summary>Derived gates</summary>

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `10eb82ed`: all 80 derived families exit
0. `--ran` reconciliation: "80 derived famil(ies) accounted for — 80
run, 0 NOT-MEASURED". `node scripts/check-platform-checklist.mjs
--changed-from origin/main`: OK, 0 anchors lost.
</details>

## Human Minimum

None; the seat lands it.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmSLoDy6zFb6HpabaTT)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

chore(objectql, plugin-sharing): one PackageScopedNameRefusedError class for the 409 a package-scoped by-name read refuses with

2 participants