Repository navigation
fix(service-datasource)!: an object imported from a package's external datasource is saved into that package or its customization package - #30602
Conversation
…or its customization pair Claude-Session: https://claude.ai/code/session_01Utp6CbDkdjhP7yRbNoEsZu Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbDkdjhP7yRbNoEsZu Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbDkdjhP7yRbNoEsZu Co-authored-by: Claude <noreply@anthropic.com>
… comment Claude-Session: https://claude.ai/code/session_01Utp6CbDkdjhP7yRbNoEsZu Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 33 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 14 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 3 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 30c41ab2cf5c1418e009eee1a827db5266d1b452 && git checkout 30c41ab2cf5c1418e009eee1a827db5266d1b452
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bdbaf80cdb01ececd5af069757fb1f35c653bb8a 100d47a7055d824e7733fc68075a2732d6c2a6b9 && git checkout -B drift-repro bdbaf80cdb01ececd5af069757fb1f35c653bb8a && git merge --no-ff 100d47a7055d824e7733fc68075a2732d6c2a6b9
node scripts/docs-audit/affected-docs.mjs --json bdbaf80cdb01ececd5af069757fb1f35c653bb8a
|
|
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE Contract-review-claim: session_01Utp6CXCS98uhsFQYk64epE · head 100d47a · 2026-11-05T17:42Z |
|
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE Contract reviewServed-tier: Inputs: card #30469 body and all 8 comments (ruling B is record 6660988968; the dev's os-dev-report is 6664934445), PR #30602 body, file list and the net diff ① Derived judgmentsAccept-set moves at
② Semver level
③ Boundary flagsFrom the os-dev-report (6664934445):
④ Design judgmentThe narrowing against the published spec on Attempts to break it, and what held:
should-fix (one card, should-fix (one card): one security observation handed to the seat out of band (pre-existing at this door on No must-fix. Implemented-by: VERDICT: PASS |
|
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE 席位记录 · 2026-11-05T18:04Z · Head-sha
|
Requested by Bill · project thread
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
Fixes #30469
Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: none —
git grep -n -i -E "datasource (table )?(sync|import)|external object|import_object|customization pair|customization package|\.custom" -- skillson8c52a7afanswers lines about overrides, object extensions and the permission-set clone landing in a base's.custompackage (skills/objectstack-platform/SKILL.md:128,skills/objectstack-data/SKILL.md:366,skills/objectstack-automation/SKILL.md:51and their evals) and the datasources catalog entry (skills/objectstack-data/SKILL.md:47);skills/objectstack-data/rules/datasources.mdnames no import. No published sentence says where an imported object is saved or what it is named.维护者速读
包名.custom,并跟着该包导出。只读包下导入的对象名改为定制包前缀,例如 showcase 的customers从showcase_customers变成showcase_custom_customers。packages/services/service-datasource/src/plugin.ts(resolveImportLanding:三行归属与墙内分支)、external-datasource-service.ts(importObject先定落点、最后才建定制包再保存),再看src/__tests__/external-namespace-reads-engine-registry.test.ts末尾新增的 describe。pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/external-namespace-reads-engine-registry.test.ts -t "lands in the package of its datasource",预期 14 条通过。What changes
Ruling B on where an imported object lives (ruling record 6660988968, maintainer 「同意」), built as the seat fixed it for walled postures.
_packageId(plugin.ts, newresolveImportLanding):sys_metadatasentinel, or a datasource that cannot be read → a package-less row, as before (ADR-0146 D11). The save request is byte-for-byte the old one (nopackageIdkey);isWritablePackageon the'protocol'service) → saved into that package, named in its namespace;BASE.custom, named in the pair's namespace. The pair comes from the ensure door (ensureCustomizationPackageon the same'protocol'service), the only creation path (ADR-0146 D3).importObject. The landing is resolved first (a landing that cannot be had refuses before any remote introspection); an explicitnameis judged against the landing's namespace; then the draft; thenprepare(the ensure door) runs immediately before the save. A refused name, a missing remote table or a refused draft therefore makes no pair and saves nothing.group,isolated). There the platform operator alone creates a customization package, through the ensure door's HTTP route (ADR-0146 D3, ADR-0131 D3). So the import never creates a pair under a wall: a pair that already exists receives the import; while none does, the import is refused (400 EXTERNAL_IMPORT_ERROR, the code the import door answers every refusal with) with a message naming the platform operator andPOST /api/v1/packages/BASE/ensure-customization. It is never saved package-less instead (ADR-0070 D3). The posture is thetenancyservice's effective one, through the sharedclassifyAdmissionTenancyPosture.isWritablePackage/ensureCustomizationPackage) refuses an import over a packaged datasource rather than guessing.…/tables/:remote/draftstill names the object in the datasource package's own namespace, because its*.object.tsis committed into that package's source.ExternalDatasourceServiceConfiggains the optionalresolveImportLanding;persistObjecttakes an optionalpackageId;ImportLandingis exported. A host that wires no landing keeps the old placement.The name, measured before it was fixed
On a booted showcase at
1fd0c32c(scratch file, not committed):ensureCustomizationPackage({ basePackageId: 'com.example.showcase' })answeredcom.example.showcase.custom, namespaceshowcase_custom. A save ofshowcase_customersinto the pair was refused422 NAMESPACE_PREFIX("Rename it to 'showcase_custom_showcase_customers'");showcase_custom_customerswas saved, listed with_packageId: com.example.showcase.custom, and carried byGET /packages/com.example.showcase.custom/exportandGET /packages/export. The pair does not share the base's namespace at the save door, so the name isBASE_NS_custom_TABLE.Tests
external-namespace-reads-engine-registry.test.ts, new top-leveldescribe('[#30469 item 2] an import lands in the package of its datasource'), 14 cases): package-less andsys_metadatacontrols; writable package; read-only base → ensure then save into the pair, in that order; an explicit base-namespace name refused with the validator's message for the pair, before the pair and before introspection; a missing remote table makes no pair; an ensure refusal refuses the import and saves nothing; forgroupandisolated, the import never creates the pair and refuses naming the platform operator, and an existing pair receives the import;singlecontrol; a protocol without the landing doors refuses; the draft door keeps the base namespace. Four existing cases in the same file now give their protocol fake the writability answer.external-import-code-datasource-namespace.dogfood.test.ts): an import with no name lands asshowcase_custom_customersincom.example.showcase.custom, both exports carry it, its rows read, and a package-lessshowcase_customerssaved before stayssys_metadataand outside the pair's export; a new top-leveldescribeboots undergroup(multiTenant: 'posture-only') and pins the refusal (nothing saved, no pair), then the operator's ensure door, then the import landing in the pair.showcase_externalunder explicit names; their names now carryshowcase_custom_(external-federation-read-capability,external-import-destructive-remedy,external-import-saves-like-meta(cold boot),external-validate-sees-runtime-save). All five files: 24/24 + the new cases green.898145b6(source restored to the base, tests kept): unit 10 failed / 15 passed of the file's 25 (the 10 non-control new cases; the 4 new controls and the 11 older cases pass); showcase 4 failed / 7 passed (expected 'showcase_customers' to be 'showcase_custom_customers'; the walled import answered201where400is pinned).scripts/ablation-replace.mjs): the walled condition replaced byfalse(anchor ×1 → ×0, marker ×0 → ×1) → the four walled cases red (Tests 4 failed | 21 passed); restored blob09eaac88= HEAD,git diff HEADempty.Acceptance notes
toLabel(name), unchanged, so an import over a read-only base now readsShowcase Custom Customers. The label rule is not touched here.400 EXTERNAL_IMPORT_ERRORwith the door's sentence; nothing is saved.Verification
Package suite and typecheck
On
8c52a7af(after theorigin/mainmerge; the one later commit100d47a7rewords a comment inplugin.ts):pnpm --filter @objectstack/service-datasource run typecheckexit 0;vitest run --maxWorkers=2: Test Files 72 passed (72), Tests 1205 passed (1205).Full dogfood battery (BREAKING narrowing)
pnpm --filter @objectstack/dogfood testunder the verify lock on8c52a7afwith the closure built: Test Files 420 passed, 3 skipped (423); Tests 3500 passed, 36 skipped (3536);VERDICT command-exit 0. The tree differs from the head100d47a7by one comment inplugin.ts.Derived gates (head 100d47a)
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 66 commands at100d47a7; each ran with its exit code recorded before any pipe;--ran: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN, every exit 0.pnpm lintexit 0.node scripts/pm/check-governed-merges.mjs --teston the final file list: 0 of 10 paths governed, 667 changed lines.Generated by Claude Code