Skip to content

fix(service-datasource, rest): the datasource Sync doors and federation twins answer an unknown name 404 RESOURCE_NOT_FOUND - #27693

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-bvrf7w
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-bvrf7w

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Requested by Sales · project thread

Fixes #27618

Clause-②: no
Metadata-protocol: none
Skills-docs: none — the kind is none and there is no narrowing arm; no skills/** or content/docs page states what these doors answer for an unknown datasource name (git grep for remote-tables, object-draft, refresh-catalog, has no datasource definition outside the release notes: no such sentence).

Claim: 6504788091 (seat domain:server#2, branch claude/project-thread-bvrf7w, the card thread's harness-pinned branch). Triage direction: 6500612695. Ruling: none.

维护者速读

  • 改了什么:数据源的「同步」入口(列远端表、生成对象草稿)以及 /external/ 下的四个联邦入口(列表、草稿、导入、刷新目录),对一个根本不存在的数据源名,现在统一回 404 RESOURCE_NOT_FOUND,措辞与 GET /datasources/NAME 完全一致;包内「数据源不存在」的报错收成一个共用函数。
  • 为什么改:之前这些入口对不存在的名字回 400,还附一段「请把数据源声明为 external」的说教,管理工具、脚本和 AI 调用方会误以为是请求写错了,而其它按名入口早已回 404。
  • 风险与回滚:只改错误状态码与文案,正常路径不动;已存在但不是联邦源的数据源(托管的、主数据源)仍回原来的 400。回滚即 revert 本 PR。
  • 先看:packages/services/service-datasource/src/external-datasource-service.ts(assertFederationSource 新增的「无定义」分支)、packages/rest/src/external-datasource-routes.ts(answerThrown 的 404 透传)、packages/services/service-datasource/src/datasource-admin-service.ts(唯一的 datasourceNotFoundError)。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/external-datasource-unknown-name.test.ts,预期 12 个用例全绿(4 个入口回 404,8 个对照仍回 400)。
  • 需要你做的:无,席位落地。

What changes

  • ExternalDatasourceService.assertFederationSource (the guard every federation door runs first) now splits the two cases it folded together: a name with no datasource definition throws the 404 RESOURCE_NOT_FOUND envelope (Datasource "NAME" does not exist.), and a definition that is not a federation source keeps its 400 with the door's code (EXTERNAL_DATASOURCE_ERROR, or EXTERNAL_IMPORT_ERROR for the import). The primary datasource is still refused first, by name, with its own 400. notAFederationSourceError no longer takes an absent definition, so its dead "has no datasource definition" arm is gone.
  • One helper, not copies. The two private datasourceNotFoundError copies (admin service, external service) are merged into one in datasource-admin-service.ts, exported from that module for its siblings only. ⛔ It is not added to the package index (src/index.ts uses named exports only and is unchanged), so the published surface does not move. GET /datasources/NAME in admin-routes.ts now takes its 404 words from the same helper, so every by-name door spells the answer once.
  • service-datasource's admin routes already relayed that envelope as 404 (badRequest), so GET …/remote-tables and POST …/object-draft needed no route change, only the guard.
  • packages/rest's federation family answered every non-fault throw 400. answerThrown now relays the 404 / RESOURCE_NOT_FOUND envelope as such (both fields required, as in badRequest), so GET …/external/tables, POST …/external/tables/REMOTE/draft, POST …/external/tables/REMOTE/import and POST …/external/refresh-catalog answer an unknown name as GET /datasources/NAME does. Every other throw keeps this family's 400 and code.

Tests

  • New service-datasource/src/__tests__/federation-doors-unknown-name.test.ts, over the real composition (real ObjectQL, connection service, admin service, ExternalDatasourceServicePlugin wiring, admin routes on Hono, an entitled caller): GET /datasources/no_such_ds/remote-tables and POST /datasources/no_such_ds/object-draft answer status, code and message equal to GET /datasources/no_such_ds (404 RESOURCE_NOT_FOUND); controls: a managed datasource and the primary datasource still answer 400 EXTERNAL_DATASOURCE_ERROR with their reasons.
  • New rest/src/external-datasource-unknown-name.test.ts, the real service under the real routes on Hono: the four /external/ twins answer 404 RESOURCE_NOT_FOUND, Datasource "no_such_ds" does not exist. (GET's words); controls: managed and primary datasources still answer 400 with each door's own code.
  • Moved pins (they held the folded answer this card reverses): managed-datasource-sync-refusal.test.ts (the ghost case now expects the 404) and external-metadata-read-at-use.test.ts (the no-metadata-service case now expects the 404 envelope from all four service methods).
  • Red first: before the fix, the 2 new service-datasource 404 pins and the 4 new rest 404 pins were red (400 EXTERNAL_DATASOURCE_ERROR / EXTERNAL_IMPORT_ERROR, "has no datasource definition …"), the controls green.

Acceptance notes

  • Ablation, two legs, each red then restored (scripts/ablation-replace.mjs, fix committed first, restore blob == HEAD and git diff HEAD empty). Both suites read source (service-datasource tests import src/ directly; rest's vitest.config.ts aliases @objectstack/service-datasource to its src/index.ts), so no dist/ rebuild is in either leg.
    • Leg 1, delete if (!ds) throw datasourceNotFoundError(datasource); in assertFederationSource: service-datasource 2 failed / 4 passed, rest 4 failed / 8 passed (every 404 pin red, every 400 control green). Restored blob 922e07c2df3d == HEAD.
    • Leg 2, delete the 404 relay in rest's answerThrown: rest 4 failed / 8 passed, received 400 EXTERNAL_DATASOURCE_ERROR / 400 EXTERNAL_IMPORT_ERROR. Restored blob 1629a50c6b7e == HEAD.
  • A deployment with no metadata service at all cannot read any datasource definition, so these doors now answer such a name 404 where they answered the "no definition" 400. That is the reading POST /datasources/NAME/test already makes since fix(service-datasource)!: POST /datasources/:name/test answers an unknown name 404 RESOURCE_NOT_FOUND #27574 (it resolves through the same getDatasource read), so the by-name doors stay one answer; noted, not a change of direction.
  • Observation, not filed: POST /datasources/NAME/external/validate does not run the federation guard (it lists the federated objects bound to NAME), so an unknown name answers 200 with an empty report. That is code reading only (no door measured), outside this card's doors.
  • Branch name is the card thread's harness-pinned claude/project-thread-bvrf7w, not the claude/issue-N-slug convention; the claim above is its identity record.

Verification

All on head 2d19915c (the fix commit 88e7fb01 merged with origin/main at 50d05cb1).

Package suites and typecheck
  • pnpm --filter @objectstack/service-datasource test && … typecheck under os-verify-lock.sh: VERDICT command-exit 0, Test Files 61 passed (61), Tests 1010 passed (1010).
  • pnpm --filter @objectstack/rest test && … typecheck under os-verify-lock.sh: VERDICT command-exit 0, Test Files 340 passed (340), Tests 6674 passed, 326 skipped (7000).
  • eslint --no-inline-config on the 8 changed source and test files: exit 0.
  • pnpm lint (repo-wide eslint . --no-inline-config) on 2d19915c: exit 0, no findings.
Derived gates
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 62 families derived at 2d19915c. All 62 run, each exit 0 (check:dual-build-cjs-loads first answered exit 3, a prerequisite with 48 dists missing; after a full turbo run build it answered exit 0: 112 require entry points across 69 packages load).
  • --ran reconcile: 62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED.
  • No open PR edits these files (REST scan of the 34 open PRs' file lists), so no merge-tree pair was owed.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgXTXpQTz45k8v4WACt

…e name 404 RESOURCE_NOT_FOUND

The federation guard folded a name with no datasource definition into the
"not a federation source" 400. It now throws the package's one
datasourceNotFoundError (the two private copies are merged into one helper
in datasource-admin-service.ts, not exported from the package index), and
the rest federation twins relay that 404 envelope as the admin routes do.
A definition that is not a federation source keeps its 400.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/service-datasource, touching 9 documentable anchor(s).

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError; a string literal in registerDatasourceAdminRoutes; a string literal in registerExternalDatasourceRoutes))
  • content/docs/api/error-catalog.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError; a string literal in registerDatasourceAdminRoutes; a string literal in registerExternalDatasourceRoutes))
  • content/docs/api/error-handling-client.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError; a string literal in registerDatasourceAdminRoutes; a string literal in registerExternalDatasourceRoutes))
  • content/docs/automation/webhooks.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError; a string literal in registerDatasourceAdminRoutes; a string literal in registerExternalDatasourceRoutes))
  • content/docs/data-modeling/drivers.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment in registerExternalDatasourceRoutes; a path literal in a comment on a changed line))
  • content/docs/kernel/cluster.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment in registerExternalDatasourceRoutes; a path literal in a comment on a changed line))
  • content/docs/kernel/contracts/metadata-service.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError; a string literal in registerDatasourceAdminRoutes; a string literal in registerExternalDatasourceRoutes))
  • content/docs/permissions/sso.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError; a string literal in registerDatasourceAdminRoutes; a string literal in registerExternalDatasourceRoutes))
  • content/docs/permissions/system-context.mdx (via registerExternalDatasourceRoutes (symbol, a top-level function))
  • content/docs/protocol/kernel/error-handling.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError; a string literal in registerDatasourceAdminRoutes; a string literal in registerExternalDatasourceRoutes), /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment in registerExternalDatasourceRoutes; a path literal in a comment on a changed line))
  • content/docs/ui/print-pages.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError; a string literal in registerDatasourceAdminRoutes; a string literal in registerExternalDatasourceRoutes))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError; a string literal in registerDatasourceAdminRoutes; a string literal in registerExternalDatasourceRoutes))
  • content/docs/releases/v17/17-1.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment in registerExternalDatasourceRoutes; a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-5.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError; a string literal in registerDatasourceAdminRoutes; a string literal in registerExternalDatasourceRoutes))
  • content/docs/releases/v17/17-6.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment in registerExternalDatasourceRoutes; a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-7.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment in registerExternalDatasourceRoutes; a path literal in a comment on a changed line))
  • content/docs/releases/v18/18-2.mdx (via RESOURCE_NOT_FOUND (literal, a string literal in datasourceNotFoundError; a string literal in registerDatasourceAdminRoutes; a string literal in registerExternalDatasourceRoutes), /datasources/:name (route, a path literal in a comment in ExternalDatasourceService; a path literal in a comment in registerExternalDatasourceRoutes; a path literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 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 — 21 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 bfdaba4322e1258a7657aa622af021e31ab071aa → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 bfdaba4322e1258a7657aa622af021e31ab071aa → 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-sales · session_01L6bhgUCizoYWQVJWjhdR6F

维护者速读(席位终稿)

  • 改了什么:数据源的同步门(remote-tables、object-draft)和 /external/ 下的联邦孪生门(tables、draft、import、refresh-catalog)遇到不存在的数据源名字,改回 404 RESOURCE_NOT_FOUND,和 GET /datasources/NAME 一样,不再回 400 联邦拒绝。
  • 为什么改:原来的 400 让调用方以为请求格式错了、或要把数据源改声明成 external,其实是这个数据源根本不存在。
  • 风险与回滚:只换「名字不存在」这一种拒绝的状态码与错误码;存在但不是联邦源的数据源仍回 400 原码原因;包的公开导出不变。回滚 = revert 本 PR。
  • 先看:external-datasource-service.ts 的 assertFederationSource 多出的一行,rest/src/external-datasource-routes.ts 的 answerThrown 404 透传。
  • 两分钟验证:pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/federation-doors-unknown-name.test.ts。
  • 需要你做的:无。非受管面,席位落地。

席位意见: ACCEPT。拒绝本来就有,只是换成和读门一致的 404;一个包里只剩一个「数据源不存在」的写法。

席位核对

ACCEPT · head 2d19915cdc0c3038e47676735a6ab4cd8f659e96 · 2026-10-29T19:32Z

  • 形态: 9 files, +369/−46 across service-datasource and rest, two new test files, two existing pins moved, one changeset. No packages/spec, no governed path; Tier S.
  • Clause-②: no (seat's reading): every name these doors refused before is still refused, nothing new is accepted or refused; only the refusal for a name with no definition moves from 400 EXTERNAL_DATASOURCE_ERROR / EXTERNAL_IMPORT_ERROR to the 404 RESOURCE_NOT_FOUND the by-name read already answers. The changeset states the before / now / unchanged. Metadata-protocol: none.
  • 前提实测: the new door pins red on main (the absent definition was folded into the non-federation 400); green on the fix; two ablation legs red, restored to the HEAD blob.
  • 抽查读数: the seat read the hunks: assertFederationSource keeps the primary-datasource refusal first, then if (!ds) throw datasourceNotFoundError(datasource), then the non-federation 400 (whose ds is now non-optional); the two private helper copies are one module-level export of datasource-admin-service.ts, not re-exported from the package index; GET /datasources/:name spells its 404 through the same helper; answerThrown relays a 404 only when both status === 404 and code === 'RESOURCE_NOT_FOUND' are set. The two moved pins (no metadata service; ghost name) keep their intent (refused before any introspection) and now assert the 404.
  • 门禁 check-runs: 48 on this head: 34 success, 14 skipped, 0 other (read 2026-10-29T19:32Z); mergeable_state: clean. Derived gates 62/62, 0 NOT-MEASURED; service-datasource 1010/1010, rest 6674 green; pnpm lint exit 0.
  • 接受的偏差: PR footer and attribution line name the card thread's session (the thread's own attribution requirement); the seat patches the header to the seat signature and seat thread.
  • 范围外去向: POST /datasources/:name/external/validate skips the federation guard and answers an unknown name 200 with an empty report → datasources: POST /datasources/NAME/external/validate answers an unknown name 200 with an empty report, where every other by-name datasource door answers 404 RESOURCE_NOT_FOUND #27696.
  • 落地条款: relay pr_ready + automerge_enable through the merge queue.

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

Projects

None yet

1 participant