Repository navigation
refactor(core,objectql,plugin-sharing): one PackageScopedNameRefusedError class for every package-scoped by-name refusal - #31876
Conversation
…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>
Claude-Session: https://claude.ai/code/session_01ADHkmQHXhv9SKTU1qQ9CNC Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 52 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 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
|
|
domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM Contract reviewServed-tier: Inputs: card #31852 (body and the seat's delta), PR #31876 body and file list, and the net diff of ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM 维护者速读
席位核对 — ACCEPT · domain:server#2 · Head-sha
|
…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>
Requested by Steve · project thread
Fixes #31852
Clause-②: yes (widening) —
@objectstack/coreand the lean entry@objectstack/objectql/coregainPackageScopedNameRefusedErrorMetadata-protocol: none
维护者速读
PackageScopedNameRefusedError原本在 objectql 和 plugin-sharing 各有一份,现在只在@objectstack/core声明一次,两个包原名转出同一个类;objectql 的精简入口@objectstack/objectql/core也导出它。导航翻译检查脚本遇到同名应用被拒时,改为报一条列出所有候选包的组合错误,不再抛出 409 中断。instanceof,以后谁要捕获"包作用域名字冲突"就得猜用哪一份,猜错一半;精简入口的使用者只能按 code 和 status 匹配。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):PackageScopedNameRefusedErroris declared here once, besidePackageScopedRefusalandpackageScopedRefusalMessage. It carries the union of the two old copies' fields:code = 'RESOURCE_CONFLICT',status = 409,httpStatus = 409andrefusal, 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.tsandregistry.tskeep their imports.@objectstack/objectql/core(lean entry,core.ts): exports the class too, since theSchemaRegistryit exports throws it fromgetApp. Core is already in the entry's closure, socore-boundary.ratchet.test.tsstays green.@objectstack/plugin-sharing(package-scoped-declarations.ts): the local class is gone; the module re-exports core's binding under the same name, sosharing-rule-service.tsandownership-queue-service.tskeep their imports. Its refusals gainhttpStatus: 409.packages/cli/scripts/check-app-nav-i18n.mjs(N1): the app is read through a new purereadMergedApp(name, getApp, isRefusal). APackageScopedNameRefusedErrorfromregistry.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"onorigin/main074a014c: two declarations (objectqlpackage-scoped-read.ts, plugin-sharingpackage-scoped-declarations.ts); throws inpackage-scoped-read.ts,sharing-rule-service.ts(2) andownership-queue-service.ts(2);instanceofinmetadata-facade.ts,sharing-rule-service.tsandownership-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 409RESOURCE_CONFLICTenvelope, different name, extratemplateandcandidatesfields), inplugin-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/clitakes none: itsfilesisdist,README.mdandCHANGELOG.md, soscripts/does not publish.Tests
packages/core/src/package-scoped-resolution.test.ts: the class carriescode,status,httpStatus,refusaland the refusal's sentence.packages/objectql/src/package-scoped-e8-readers.test.ts:toBeidentity of the class frompackage-scoped-read.js, the barrel and the lean./coreentry against core's; agetApprefusal isinstanceofthe lean entry's class.packages/plugins/plugin-sharing/src/package-scoped-declarations.test.ts:toBeidentity of plugin-sharing's and objectql's export against core's; a refusal from objectql's by-name reader (SchemaRegistry#getApp) isinstanceofplugin-sharing's class, and a refusal from plugin-sharing's door (SharingRuleService#declaredRuleNamed) isinstanceofobjectql's class.check-app-nav-i18n.mjs --self-test: six new cases forreadMergedApp(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).package-scoped-e8-readers.test.ts("the refusal crosses a wire door as 409 RESOURCE_CONFLICT") and plugin-sharingpackage-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
EmailTemplateAmbiguousError, a third class for the same refusal family. Not touched here (ruling 1); its seat decides whether it extends or reuses this class.Verification
Head
3bb5c3e6(02e922a7plus a merge oforigin/main).Red first, on origin/main 074a014 (tests added, fix not yet applied)
package-scoped-resolution.test.ts:Tests 1 failed | 23 passed (24).package-scoped-e8-readers.test.ts:Tests 2 failed | 71 passed (73).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)
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).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=2under the lock at02e922a7:VERDICT command-exit 0,Tasks: 81 successful, 81 total.02e922a7, one lock run:typecheckfor core, objectql, plugin-sharing, thenpnpm --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.3bb5c3e6(after the merge; incoming main touched other files of core and objectql): rebuild of the three packages, theirtypecheck, and the three changed test files pluscore-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-configonpackages/core/src,packages/objectql/src,packages/plugins/plugin-sharing/srcand 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 at02e922a7. 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 at3bb5c3e6: see the report comment on the card.Generated by Claude Code