Skip to content

fix(service-datasource)!: an object imported from a package's external datasource is saved into that package or its customization package - #30602

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-0aje2v
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-0aje2v

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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" -- skills on 8c52a7af answers lines about overrides, object extensions and the permission-set clone landing in a base's .custom package (skills/objectstack-platform/SKILL.md:128, skills/objectstack-data/SKILL.md:366, skills/objectstack-automation/SKILL.md:51 and their evals) and the datasources catalog entry (skills/objectstack-data/SKILL.md:47); skills/objectstack-data/rules/datasources.md names no import. No published sentence says where an imported object is saved or what it is named.

维护者速读

  • 改了什么:从外部数据源导入的表,现在按数据源归谁来存:Setup 里加的数据源照旧不进包;包声明的数据源,可写包就存进该包,只读的已装包就存进它的定制包 包名.custom,并跟着该包导出。只读包下导入的对象名改为定制包前缀,例如 showcase 的 customers 从 showcase_customers 变成 showcase_custom_customers。
  • 为什么改:你 14:46Z 裁定 B(裁定记录 6660988968)。之前导入的对象名字说属于某个包,存储却不属于任何包,包导出带不走,换环境就丢。
  • 风险与回滚:BREAKING 收窄:只读包数据源上用旧前缀的显式名字现在被拒(提示里给出新名字);墙内形态(group/isolated)下定制包还不存在时导入被拒,提示请平台运营方先建;已导入的旧对象不迁移、不改名,重新导入会得到新名字的对象,旧的由管理员删。回滚即 revert 本 PR。
  • 先看: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 条通过。
  • 需要你做的:无,席位落地。ADR-0146 D3「It receives」加一项、D11 加半句仍是你的 Tier H 待办(本 PR 不改 ADR)。

What changes

Ruling B on where an imported object lives (ruling record 6660988968, maintainer 「同意」), built as the seat fixed it for walled postures.

  • Placement follows the datasource's _packageId (plugin.ts, new resolveImportLanding):
    • no package, the sys_metadata sentinel, 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 (no packageId key);
    • a writable package (the save door's own ADR-0070 predicate, isWritablePackage on the 'protocol' service) → saved into that package, named in its namespace;
    • a read-only installed base → saved into its customization package BASE.custom, named in the pair's namespace. The pair comes from the ensure door (ensureCustomizationPackage on the same 'protocol' service), the only creation path (ADR-0146 D3).
  • Ordering in importObject. The landing is resolved first (a landing that cannot be had refuses before any remote introspection); an explicit name is judged against the landing's namespace; then the draft; then prepare (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.
  • Walled postures (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 and POST /api/v1/packages/BASE/ensure-customization. It is never saved package-less instead (ADR-0070 D3). The posture is the tenancy service's effective one, through the shared classifyAdmissionTenancyPosture.
  • A protocol that cannot answer (no isWritablePackage / ensureCustomizationPackage) refuses an import over a packaged datasource rather than guessing.
  • The draft door is unchanged: …/tables/:remote/draft still names the object in the datasource package's own namespace, because its *.object.ts is committed into that package's source.
  • ExternalDatasourceServiceConfig gains the optional resolveImportLanding; persistObject takes an optional packageId; ImportLanding is exported. A host that wires no landing keeps the old placement.
  • Existing package-less imported objects are not migrated or renamed. Re-importing the same table makes a new object under the new name beside the old one; the administrator deletes the old one (pinned on the showcase below).

The name, measured before it was fixed

On a booted showcase at 1fd0c32c (scratch file, not committed): ensureCustomizationPackage({ basePackageId: 'com.example.showcase' }) answered com.example.showcase.custom, namespace showcase_custom. A save of showcase_customers into the pair was refused 422 NAMESPACE_PREFIX ("Rename it to 'showcase_custom_showcase_customers'"); showcase_custom_customers was saved, listed with _packageId: com.example.showcase.custom, and carried by GET /packages/com.example.showcase.custom/export and GET /packages/export. The pair does not share the base's namespace at the save door, so the name is BASE_NS_custom_TABLE.

Tests

  • Unit (external-namespace-reads-engine-registry.test.ts, new top-level describe('[#30469 item 2] an import lands in the package of its datasource'), 14 cases): package-less and sys_metadata controls; 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; for group and isolated, the import never creates the pair and refuses naming the platform operator, and an existing pair receives the import; single control; 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.
  • Showcase (external-import-code-datasource-namespace.dogfood.test.ts): an import with no name lands as showcase_custom_customers in com.example.showcase.custom, both exports carry it, its rows read, and a package-less showcase_customers saved before stays sys_metadata and outside the pair's export; a new top-level describe boots under group (multiTenant: 'posture-only') and pins the refusal (nothing saved, no pair), then the operator's ensure door, then the import landing in the pair.
  • Four more showcase files import over showcase_external under explicit names; their names now carry showcase_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.
  • Red on the base 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 answered 201 where 400 is pinned).
  • Ablation (once, scripts/ablation-replace.mjs): the walled condition replaced by false (anchor ×1 → ×0, marker ×0 → ×1) → the four walled cases red (Tests 4 failed | 21 passed); restored blob 09eaac88 = HEAD, git diff HEAD empty.

Acceptance notes

  • The import's object label is toLabel(name), unchanged, so an import over a read-only base now reads Showcase Custom Customers. The label rule is not touched here.
  • The ensure door's own refusals (a base with no namespace, a non-SemVer version) reach the import caller as the import door's 400 EXTERNAL_IMPORT_ERROR with the door's sentence; nothing is saved.
  • ADR-0146 text (D3 "It receives" gains imports over the base's datasource; D11 "environment metadata" gains imports over a Setup-added datasource) is the maintainer's Tier H todo and is not edited here.

Verification

Package suite and typecheck

On 8c52a7af (after the origin/main merge; the one later commit 100d47a7 rewords a comment in plugin.ts): pnpm --filter @objectstack/service-datasource run typecheck exit 0; vitest run --maxWorkers=2: Test Files 72 passed (72), Tests 1205 passed (1205).

Full dogfood battery (BREAKING narrowing)

pnpm --filter @objectstack/dogfood test under the verify lock on 8c52a7af with 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 head 100d47a7 by one comment in plugin.ts.

Derived gates (head 100d47a)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 66 commands at 100d47a7; each ran with its exit code recorded before any pipe; --ran: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN, every exit 0. pnpm lint exit 0. node scripts/pm/check-governed-merges.mjs --test on the final file list: 0 of 10 paths governed, 667 changed lines.


Generated by Claude Code

@github-actions github-actions Bot added the size/l label Nov 5, 2026
@github-actions github-actions Bot added 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 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

33 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 bdbaf80cdb01ececd5af069757fb1f35c653bb8a.

⛔ 14 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/services/service-datasource/src/index.ts) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: packageId (4 routes)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 3 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 bdbaf80cdb01ececd5af069757fb1f35c653bb8a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 30c41ab2cf5c1418e009eee1a827db5266d1b452 — the merge of head 100d47a7055d824e7733fc68075a2732d6c2a6b9 into base bdbaf80cdb01ececd5af069757fb1f35c653bb8a, 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 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

⚠️ 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 bdbaf80cdb01ececd5af069757fb1f35c653bb8a → 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#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE

Contract-review-claim: session_01Utp6CXCS98uhsFQYk64epE · head 100d47a · 2026-11-05T17:42Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 100d47a7055d824e7733fc68075a2732d6c2a6b9
Local-runs: none
Scope: packages/services/service-datasource/src/plugin.ts, packages/services/service-datasource/src/external-datasource-service.ts, packages/services/service-datasource/src/index.ts, packages/services/service-datasource/src/tests/external-namespace-reads-engine-registry.test.ts, packages/qa/dogfood/test/external-import-code-datasource-namespace.dogfood.test.ts, packages/qa/dogfood/test/external-federation-read-capability.dogfood.test.ts, packages/qa/dogfood/test/external-import-destructive-remedy.dogfood.test.ts, packages/qa/dogfood/test/external-import-saves-like-meta.dogfood.test.ts, packages/qa/dogfood/test/external-validate-sees-runtime-save.dogfood.test.ts + packages/metadata-protocol/src/package-writability.ts, packages/metadata-protocol/src/protocol-customization-package.ts, packages/metadata-protocol/src/protocol-save.ts (NAMESPACE_PREFIX and the ADR-0070 writability gate), packages/metadata-protocol/src/protocol-tenant-authored-write.ts, packages/core/src/security/admission-tenancy-posture.ts, packages/core/src/security/authz-store-unavailable.ts, packages/spec/src/security/tenancy-posture.ts, packages/spec/src/kernel/customization-package.ts, packages/spec/src/contracts/external-datasource-service.ts, packages/rest/src/external-datasource-routes.ts, packages/rest/src/error-response.ts, packages/runtime/src/domains/packages.ts (ensure route, refuseWalledCatalogPackageWrite), packages/runtime/src/domains/mcp.ts, packages/mcp/src/mcp-http-tools.ts, packages/services/service-datasource/src/admin-routes.ts, docs/adr/0146 D2/D3/D11, docs/adr/0070 D3, docs/adr/0131 D3, docs/adr/0028, content/docs/data-modeling/odata-datasources.mdx, .changeset/30469-import-lands-in-datasource-package.md (read for ②, not reviewed). Not reviewed: nothing else is in the diff (10 files, 0 governed, no lockfile or generated output).

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 bdbaf80c…main...100d47a7, and the head's 46 check-runs. Gate verdicts at review time: every completed run is success or skipped (Check Changeset — which carries the ADR-0087 disposition step —, Governed Surface Queue Guard, Dogfood Regression Gate, Test UI, Check PR Size, Spec property liveness, PR-body guards, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests); still in_progress, not waited on: Build Core, Test Core (1–8/8), Type Check · (source gates, workspace, debt ledger, consumer gates), Lint · (ESLint, repo & tooling gates, docs & code guards, CI/release/engine gates, scoped · query-options erasure, scoped · slot lookup & corpus), Live E2E UI. The seat lands only once those are success.

① Derived judgments

Accept-set moves at POST /datasources/:name/external/tables/:remote/import and the MCP import_object (both reach ExternalDatasourceService.importObject; the draft door is untouched):

  1. Datasource with no _packageId, the sys_metadata sentinel, or an unreadable row → package-less save, request byte-identical to before (persistObject adds packageId only when set). Right — ruling item 1; pinned by the two controls.
  2. Datasource declared by a writable package (isWritablePackage on the 'protocol' service, the save door's own ADR-0070 predicate) → saved with that packageId, named in its namespace. Right — ruling item 1; the save door's writability gate (protocol-save.ts ~L1880) admits it.
  3. Datasource declared by a read-only installed base, non-walled posture → ensureCustomizationPackage({ basePackageId }) right before the save, saved into the pair BASE.custom, named BASE_NS_custom_TABLE. Right — the name is the one objectNamespacePrefixViolation judges against the WRITTEN package (protocol-save.ts ~L928), measured by the dev and pinned by the showcase dogfood; the pair comes from the one creation path ADR-0146 D3 names.
  4. Explicit name in the base's own namespace over a read-only base → refused 400 EXTERNAL_IMPORT_ERROR with ADR-0028's prescription for the pair namespace, before the pair and before introspection. Right, and the BREAKING narrowing — previously accepted and saved package-less under a name the pair could never hold.
  5. Walled posture (group/isolated, read through classifyAdmissionTenancyPosture), pair absent → refused 400 EXTERNAL_IMPORT_ERROR naming the platform operator and POST /api/v1/packages/BASE/ensure-customization; nothing saved, no pair made. Right — mirrors the ensure route's walled refusal (packages.ts L2755–2760, refuseWalledCatalogPackageWrite) instead of side-stepping it with a package-less row (ADR-0070 D3's forbidden fallback).
  6. Walled posture, pair present → saved into it, ensure door not asked. Right as ruled (B); the caller-side remainder is the out-of-band observation in ④.
  7. 'protocol' service without isWritablePackage/ensureCustomizationPackage and a packaged datasource → refused, nothing saved. Right — no guessing; a host without the doors keeps the old placement only through a config that wires no resolveImportLanding.
  8. Public surface (@objectstack/service-datasource): ImportLanding type exported from index.ts; ExternalDatasourceServiceConfig.resolveImportLanding?; persistObject's optional third packageId. All additive widenings, right; a host-built config without them behaves as on main (importLanding() falls back to getNamespace).
  9. plugin.ts now value-imports @objectstack/core (was type-only). Right — @objectstack/core is a runtime dependency of the package and admin-routes.ts already value-imports it.
  10. Tenancy read: kernel getServiceAsync('tenancy') with the same accessor-presence guard the datasource admin routes use; a LiteKernel host answers undefined and takes the non-walled path. Right — the per-seam justification matches admin-routes.ts L440–466.

② Semver level

.changeset/30469-import-lands-in-datasource-package.md: '@objectstack/service-datasource': patch, title with !, body **BREAKING** accept-set narrowing. Clause-②: yes (narrowing) in the PR body and the changeset, line-initial, identical — matches the diff (item ①4 and ①5 are refusals of previously accepted requests). Metadata-protocol: none — right: no key, value or type an author writes changes; ImportObjectOpts (packages/spec/src/contracts/external-datasource-service.ts) keeps its shape, and packages/spec is untouched (the ruling's item 4). none ⇒ patch per the Post-Task Checklist rule (a non-protocol breaking change ships patch with its banner and disposition); the three public-surface widenings in ①8 ride the same patch. ADR-0087 marker: exactly one, not-required (no-migration-prescription); the gate that re-validates that category against the changeset's own prose runs inside Check Changeset, which is success on this head. Skills-docs: none holds: skills/** and content/docs/data-modeling/odata-datasources.mdx name the import without saying where it lands or what it is named.

③ Boundary flags

From the os-dev-report (6664934445):

  • Five packages/qa/dogfood files outside the claimed surface. Answered: accepted. Four carry explicit import names in the base namespace that ①4 now refuses (stale fixtures the change reds, fixed in the same PR as os-dev.md allows); the fifth holds the showcase pins the dispatch asked for.
  • Walled clause built as the seat's pick A (never creates under a wall; refuses while the pair is absent) rather than relaying an ensure-door 403. Answered: accepted in substance — the ruling's three fixed points hold (never package-less, names the operator and the route, no pair made by an import). What pick A leaves open at the caller is the out-of-band observation in ④.
  • Metadata-protocol: none where the claim was provisional. Answered: right (②).
  • PR body order; model-free commit trailers. Not this review's to judge; nothing in the diff turns on them.
  • security_findings: "withheld". Per .claude/agents/os-dev.md, the public comment always reads withheld/none and the finding itself lives only in the dev's private final report. withheld rather than none means the private report carries one. Escalated to the seat: read the dev's final report's security_findings and reconcile it with the out-of-band note before landing.
  • out_of_scope_findings: ADR-0146 D3 "It receives" + D11 half-sentence — the maintainer's Tier H todo per ruling item 5, correctly not edited here; the import's label (toLabel(name) ⇒ Showcase Custom Customers) — noted, not a defect of this card.
  • open_questions: none. Confirmed; the ruling's open check (prefix rule read, not run) was closed by the dev's measurement on a booted showcase and the dogfood pin.

④ Design judgment

The narrowing against the published spec on main. IExternalDatasourceService.importObject and ImportObjectOpts declare no placement and no namespace for the saved object; the docs say "Import saves one". Nothing declared at the consumer is taken away. What the narrowing makes real are two refusals the spec's own doors already own: the save door's NAMESPACE_PREFIX rule judged against the package written (protocol-save.ts, ADR-0129 D2 / ADR-0028), which the package-less import had been slipping past, and the ensure route's walled refusal (packages.ts, ADR-0131 D12 / ADR-0146 D3), which a package-less landing would have made moot. Ordering holds under reading: landing → explicit-name verdict → draft (introspection) → prepare → save, so every refusal writes nothing and makes no pair (pinned, with an ablation of the walled condition). Ruling item 3 (no migration, old rows untouched, re-import lands beside) is pinned on the showcase.

Attempts to break it, and what held:

  • Pair present but malformed (non-walled): the derived namespace names the object, the ensure door refuses 409, the import refuses, nothing saved. Holds.
  • Writable _packageId not installed in the registry: namespace absent, save names the package, the save door's uninstalledNamedBaseRefusal refuses. Holds — no silent package-less landing.
  • Unreadable datasource row → package-less: the ruling's item 1 says so explicitly; noted that the same catch now decides placement, not only the prefix (as before).
  • single with a registered tenancy service, and a host with no async accessor: both reach the ensure door, consistent with the ensure route's single rule (manage_metadata, which the REST import route and the MCP write kind both require via federationCapabilityRefusal).

should-fix (one card, packages/rest, outside this PR's surface): the posture read can throw AuthzStoreUnavailableError (status 503, code SERVICE_UNAVAILABLE — a registered tenancy service that cannot answer, by the #16013 rule in admission-tenancy-posture.ts). external-datasource-routes.ts answerThrown relays only sandbox faults and 404 RESOURCE_NOT_FOUND, so the REST import route serves that outage as 400 EXTERNAL_IMPORT_ERROR — "your request was malformed" where the truth is "retry". Nothing is saved, so it is not permissive; but the twin in admin-routes.ts (badRequest) already relays the 503/SERVICE_UNAVAILABLE envelope, and the MCP tool relays it correctly (thrownRefusalEnvelope reads status and code). Remedy: give answerThrown the same 503 relay badRequest has, with a pin. This PR is the first thrower of that class on the federation import path, which is why it surfaces here.

should-fix (one card): one security observation handed to the seat out of band (pre-existing at this door on main; this PR's walled clause inherits it rather than creates it).

No must-fix.

Implemented-by: claude/project-thread-0aje2v
Reviewed-by: session_01Utp6CXCS98uhsFQYk64epE

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE

席位记录 · 2026-11-05T18:04Z · Head-sha 100d47a7 · ACCEPT

维护者速读(席位按 diff 校正)

  • 改了什么:从外部数据源导入的表,按数据源归属来存。Setup 里加的数据源照旧不进包;可写包声明的数据源,存进该包;只读已装包声明的数据源,存进它的定制包 包名.custom,跟着该包导出。只读包下导入的对象名换成定制包前缀,例如 showcase 的 customers 从 showcase_customers 变成 showcase_custom_customers。
  • 为什么改:Datasource table sync: the generated object draft gives its fields no label, so forms show raw column names; where the synced object lives (package, export) is unstated #30469 第 2 项,你 14:46Z 裁定 B(裁定记录 6660988968)。之前导入对象的名字说属于某个包,存储却不在任何包里,包导出带不走。
  • 风险与回滚:BREAKING 收窄,有两处变化。一是只读包数据源上沿用旧前缀的显式名字会被拒,提示里给出新名字。二是墙内形态(group/isolated)下定制包还不存在时,导入被拒,提示请平台运营方先建。已导入的旧对象不迁移、不改名。回滚即 revert 本 PR。
  • 先看:service-datasource 的 plugin.ts(resolveImportLanding)和 external-datasource-service.ts(importObject 先定落点,最后才建定制包、保存)。
  • 两分钟验证: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 条通过。
  • 需要你做的:无。非受管面、非 Tier H、667 行,收窄的 CONTRACT_REVIEW_TIER 全量复核 PASS(6665235060),技能不要求你审,席位落地。ADR-0146 D3/D11 两句仍是你的 Tier H 待办。

席位意见:同意落地。这次收窄不拿走 spec 声明过的能力:ImportObjectOpts 没有声明落点或命名。它让保存门已有的 NAMESPACE_PREFIX 规则和 ensure 门的墙内拒绝真正生效。各种拒绝都发生在建定制包和保存之前,所以被拒的导入什么都不写。

席位核对

  1. 形态:draft→ready,目标 main,Fixes #30469(item 1 已由 fix(service-datasource): fields of an object drafted from a datasource table carry a readable label #30530 落地,本 PR 交付 item 2,卡随之关闭);作者 objectstack-fleet[bot](中继)。
  2. 前提实测:报告 6664934445 在合并基上有 10 条单测红、4 条 showcase 红,消融一次转红后已还原。全量 dogfood 3500 条通过,66 道派生门禁全过。
  3. 门禁 check-runs(钉 head 100d47a7):全部 success 或 skipped,0 失败(落地前席位复读)。
  4. 接受的偏差:5 个 packages/qa/dogfood 文件在认领面外:其中 4 个是被本次收窄打红的旧显式名,1 个是派单要求的 showcase 钉。墙内分支按席位 15:06Z 选的 A 实现:从不建定制包,已有定制包就存进去。两项都接受。
  5. 范围外去向:复核的两条 should-fix 去向如下。REST 导入把 tenancy 服务故障答成 400,已立卡 External import over REST answers a tenancy-service outage as 400 instead of 503 #30609(p3)。另一条按 security-findings 规则走私有渠道,已交 security#1,不在此处写。Skills-docs: none 成立。changeset patch,Clause-②: yes (narrowing),Metadata-protocol: none,ADR-0087 not-required。
  6. 落地条款:check-governed-merges --pr 30602 结果 NOT governed,667 行 ≤ 3000,非 Tier H;按 13:47Z 规则,BREAKING 不等维护者,复核 PASS 且全绿后席位 ready + auto-merge。合并后席位在 Datasource table sync: the generated object draft gives its fields no label, so forms show raw column names; where the synced object lives (package, export) is unstated #30469 写落地并结线程。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 18:05
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 135c2fb Nov 5, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-0aje2v branch November 5, 2026 18:26
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/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant