Skip to content

fix(service-datasource)!: refuse creating a datasource named drivers, the driver-catalog route literal - #29262

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #29164

Clause-②: no (narrowing)
Metadata-protocol: none

维护者速读

  • 改了什么:新建运行时数据源时拒绝名字 drivers(与 default 同样的 400 DATASOURCE_ADMIN_ERROR,报错点名该名字);保留名单与路由注册用同一个常量,并有一条从注册器实际挂载推导的漂移测试。
  • 为什么改:GET /datasources/drivers 是驱动目录,先于 GET /datasources/:name 挂载,所以叫 drivers 的数据源能建、能列,但编辑表单永远打不开。
  • 风险与回滚:低。以前 201 的 drivers 创建现在 400(收窄);test 仍可创建;已存的 drivers 行不受影响。回滚即 revert。
  • 先看:datasource-route-reserved-names.ts(名单与理由),datasource-admin-service.ts 的 assertValidName,__tests__/datasource-route-reserved-names.test.ts 的漂移测试。
  • 两分钟验证:pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/datasource-route-reserved-names.test.ts,预期 Tests 7 passed (7)(main 上 drivers 用例红:expected 201 to be 400)。
  • 需要你做的:无,席位落地。

What changes

  • New src/datasource-route-reserved-names.ts holds DRIVER_CATALOG_SEGMENT and ROUTE_SHADOWED_DATASOURCE_NAMES (today ['drivers']): every literal segment the admin registrar mounts beside a same-method /datasources/:name route. Internal module, not exported from the package entry.
  • admin-routes.ts mounts the driver catalog from that constant (${root}/${DRIVER_CATALOG_SEGMENT}); the wire path is unchanged.
  • assertValidName (datasource-admin-service.ts) refuses each listed name after the existing default check, with the same thrown Error, so the route answers the same 400 DATASOURCE_ADMIN_ERROR. The message names the reserved name. No new error code, status or member.
  • Changeset .changeset/29164-reserve-route-shadowed-datasource-names.md: patch on @objectstack/service-datasource, BREAKING banner, ADR-0087 not-required (no-migration-prescription), both declaration lines.

Which literals shadow a name (measured on the registrar). Single-segment literals under /datasources are GET /drivers and POST /test. /:name routes exist for GET, PATCH and DELETE only. So drivers shadows GET /:name; test shadows nothing (there is no POST /:name; the saved-datasource probe is POST /:name/test, a different depth). test is therefore not reserved, and a test pins that a datasource named test is created (201) and read back by name (200).

Tests

New file src/__tests__/datasource-route-reserved-names.test.ts (7 cases). No existing test was edited.

  • Drift pin: runs registerDatasourceAdminRoutes against a capturing mock IHttpServer (the route-ledger conformance seam), derives the literal single segments that share a method with a /:name route, and requires ROUTE_SHADOWED_DATASOURCE_NAMES to equal that set. A control asserts the enumeration saw GET /:name and POST /test. A second case pins the derivation itself: POST /test beside GET /:name derives nothing, GET /test beside it derives test.
  • Door cases over the real HonoHttpServer and a real DatasourceAdminService: drivers → 400 DATASOURCE_ADMIN_ERROR, message names 'drivers', nothing written; default → same refusal, unchanged; reporting → 201 and read back by name; test → 201 and read back by name.
  • Stored row: a drivers row seeded in the store stays listed, patchable and removable.
Red on base, green on head, three ablations
  • Red (tests committed first, fix absent, 41bae08f): Tests 2 failed | 5 passed (7); drivers create: "expected 201 to be 400". (The second red was the stored-row case asserting 200 on a DELETE that answers 204; the case now asserts ok. Not a behaviour change.)
  • Green (f085c3c6): Tests 7 passed (7).
  • Every ablation ran through scripts/ablation-replace.mjs (anchor must hit, blob hashes, restore checked against HEAD). The test imports the service and registrar from source (relative paths), so no build sits between mutation and run.
  • Ablation 1: the new guard became if (false && ROUTE_SHADOWED_DATASOURCE_NAMES.includes(name)), anchor 1 → 0, blob 4ec99504 → 4226d0f7. Result Tests 1 failed | 6 passed (7): the drivers refusal case, "expected 201 to be 400". Restored blob 4ec99504 = HEAD.
  • Ablation 2: the default guard became if (false && name === 'default'), blob 4ec99504 → fd350350. Result Tests 1 failed | 6 passed (7): the default case, "expected 201 to be 400". Restored = HEAD.
  • Ablation 3 (drift pin): mounted an unreserved GET ${root}/health in admin-routes.ts, blob b77801e2 → 214d385c. Result Tests 1 failed | 6 passed (7): "expected [ 'drivers' ] to deeply equal [ 'drivers', 'health' ]". Restored blob b77801e2 = HEAD, git diff HEAD empty. A first attempt with an anchor the replacement repeated was refused by the tool (anchor count 1 → 1) and restored; it did not run.

Acceptance notes

  • Stored drivers rows. The refusal is at create only, and an update cannot rename (it keeps existing.name). A row created before this release stays listed, patchable, removable and probe-able by name; its detail read still answers the catalog. Fixing that read would mean the catalog route resolving a datasource too, a second meaning on one path, so it is left out of scope. The changeset gives the remedy: recreate under another name, rebind the objects, remove the old row.
  • Code-declared datasources. The spec's DatasourceSchema.name rule (/^[a-z_][a-z0-9_]*$/) also accepts drivers, so a datasource declared in code under that name hits the same shadowed detail read. That is the metadata accept surface, spec-lane work, so it is not touched here; it is reported to the seat.
  • Only GET /drivers is reserved. Reserving test too would refuse a common name that today works through every door. The drift pin reserves it the day a same-method /:name sibling mounts.

Verification

All readings at head 2c7aac31 (fix f085c3c6 plus a merge of origin/main at 104cd1a5). origin/main moved 24 commits after that merge; none touches packages/services/service-datasource, and PR #29192 (same file, different function) had not landed.

Package suite, typecheck, lint
  • pnpm --filter @objectstack/service-datasource test: Test Files 70 passed (70), Tests 1159 passed (1159), VERDICT command-exit 0.
  • pnpm --filter @objectstack/service-datasource typecheck: exit 0.
  • pnpm lint (repo-wide eslint . --no-inline-config): exit 0.
Derived gates (dispatch-gates.mjs --commands, 62 families)
  • 62 of 62 run locally, each exit 0; --ran reconciliation: "62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero, every line carrying its exit code).
  • Two first exited 3 on prerequisites, not on the diff: check-plugin-teardown-shape --self-test on a shallow clone (passed, 48 cases, after git fetch --unshallow origin main), and check:dual-build-cjs-loads before every package was built (passed after pnpm build, 117/117 tasks: 115 require entry points across 70 packages load).
Full dogfood battery (owed by the narrowing arm)
  • Head 2c7aac31, closure built with pnpm --filter '@objectstack/dogfood^...' build, then pnpm --filter @objectstack/dogfood test under the verify lock: VERDICT command-exit 0, Test Files 388 passed | 3 skipped (391), Tests 3208 passed | 28 skipped (3236), no failure.

Generated by Claude Code

…rst)

Adds the reserved-name list derived against the admin registrar's literal
siblings of /:name, a drift pin over the registrar's mounts, and door cases
over the real Hono server: creating `drivers` must answer 400.

Claude-Session: https://claude.ai/code/session_01ADHkmPjMhxWBYAk48z4Mam
Co-authored-by: Claude <noreply@anthropic.com>
…route literal

`GET /datasources/drivers` (the driver catalog) is mounted before
`GET /datasources/:name`, so a runtime datasource named `drivers` was
created but its detail read never reached. assertValidName now refuses every
literal the admin registrar mounts beside a same-method `/:name` route, from
the one list the registrar mounts the literal from; `default` is unchanged
and `test` stays creatable (no `POST /:name` sibling).

Claude-Session: https://claude.ai/code/session_01ADHkmPjMhxWBYAk48z4Mam
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-datasource, touching 5 documentable anchor(s).

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

  • content/docs/data-modeling/drivers.mdx (via /datasources/:name (route, a path literal in a comment in assertValidName; 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 assertValidName; a path literal in a comment on a changed line))
  • content/docs/protocol/kernel/error-handling.mdx (via /datasources/:name (route, a path literal in a comment in assertValidName; a path literal in a comment on a changed line))

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

  • content/docs/releases/v17/17-1.mdx (via /datasources/:name (route, a path literal in a comment in assertValidName; a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-6.mdx (via /datasources/:name (route, a path literal in a comment in assertValidName; 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 assertValidName; a path literal in a comment on a changed line))
  • content/docs/releases/v18/18-2.mdx (via /datasources/:name (route, a path literal in a comment in assertValidName; 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 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 f2bf29386f7ee10bbe41f78cd1f0d7eab85f6c92 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 f2bf29386f7ee10bbe41f78cd1f0d7eab85f6c92 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Contract-review-claim: session_01ADHkmQJdgarUjPLNgFBY67 · head a5f6f0c · 2026-11-02T11:31Z

…mment on its own line

Review fixes: the drivers page now says the datasource admin API refuses a
runtime datasource named `drivers` (the driver-catalog route), and the
route-shadowed-names comment in assertValidName moves off the closing-brace
line. No behaviour change.

Claude-Session: https://claude.ai/code/session_01ADHkmPjMhxWBYAk48z4Mam
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Contract-review-claim: session_01ADHkmQJdgarUjPLNgFBY67 · head 1384def · 2026-11-02T11:59Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1384def35cb35d9efaa9a36a25e695b58bfbe34f
Local-runs: none
Scope: packages/services/service-datasource/src/datasource-admin-service.ts, packages/services/service-datasource/src/admin-routes.ts, packages/services/service-datasource/src/datasource-route-reserved-names.ts, packages/services/service-datasource/src/tests/datasource-route-reserved-names.test.ts, .changeset/29164-reserve-route-shadowed-datasource-names.md + packages/spec/src/data/datasource.zod.ts, packages/services/service-datasource/src/contracts/datasource-admin-service.ts, packages/services/service-datasource/src/datasource-route-ledger.ts, packages/services/service-datasource/src/datasource-admin-plugin.ts, content/docs/data-modeling/drivers.mdx

The delta from the previously reviewed head a5f6f0cf is two hunks only: a docs sentence at drivers.mdx ~L281, and the comment at the service ~L1404 moved onto its own line. Every judgment below was re-read on this head. All check-runs are success or path-skipped, and none failed.

① Derived judgments

  • POST /api/v1/datasources with name: 'drivers' goes from 201 to 400 DATASOURCE_ADMIN_ERROR, and the message names the segment. That is right: GET ${root}/drivers is mounted before GET ${root}/:name (admin-routes.ts 695/749), so the by-name read never answered. No new code, status or export is added, and the module is internal.
  • The default refusal is unchanged. test is still 201, because only POST /test exists and there is no POST /:name.
  • The wire path is unchanged: the catalog is mounted from DRIVER_CATALOG_SEGMENT = 'drivers'. The route ledger's literal row still matches, and its conformance test would go red on a rename.
  • updateDatasource keeps existing.name (service :899), so a rename cannot mint drivers.
  • Case: NAME_RE is lowercase-only and Hono literal matching is case-sensitive, so Drivers is refused by the regex and shadows nothing.

② Semver level

The changeset gives @objectstack/service-datasource a patch with a BREAKING banner, one ADR-0087 marker (not-required (no-migration-prescription)), and a remedy for a stored row. That matches AGENTS.md: a non-protocol breaking change ships patch with a banner and a disposition. Clause-②: no (narrowing) is on both the PR body and the changeset, with Metadata-protocol: none. Both are right: DatasourceSchema.name is untouched, and only the admin REST create door narrows.

③ Boundary flags

  • test is not reserved, and that is right (option A). POST /datasources/test shares no method with any /:name route, so a datasource named test can be read, patched, removed and probed today. The drift pin is method-aware and would reserve test on the day a same-method sibling mounts.
  • A stored drivers row stays listed, patchable and removable, and its detail read is still the catalog. That is acceptable, and the changeset carries the remedy.
  • The spec's DatasourceSchema.name still accepts drivers for code-declared and metadata-saved datasources (datasource.zod.ts:652; the meta door parses the schema only). This is escalated to the spec lane as spec(data): DatasourceSchema.name accepts drivers (and the metadata save door stores it), a name the datasource admin routes can never serve by name #29308.

④ Design judgment

The narrowing takes no declared capability away. Neither the spec nor IDatasourceAdminService.createDatasource promises a name beyond the regex, and the default reservation already sits at this door. A drivers datasource was never deliverable, because its edit-form read was unreachable. So this change makes the registrar's own stated refusal real at the door that mints names.
Attempts to break it:

  • Drift: the reserved list and the mount share one constant, and the pin derives the shadowing set from the registrar's real mounts, failing both ways. It holds.
  • Other minting doors: create is guarded, update cannot rename, and restore only re-registers stored rows. The meta save door stays open; that goes to the spec lane, as above.
  • Docs: drivers.mdx now names drivers as reserved, consistent with the code and the changeset. Resolved.
  • Known boundary note, not a fix owed here: the derivation skips multi-segment literals.
    Must-fix: none.

Implemented-by: claude/project-thread-0rjenl
Reviewed-by: session_01ADHkmQJdgarUjPLNgFBY67

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

席位核对 · ACCEPT · PR #29262 · 2026-11-02T12:00Z

Head-sha: 1384def35cb35d9efaa9a36a25e695b58bfbe34f

维护者速读(席位终稿)

核对六项

  1. 形态:draft → main;标题带 !;Fixes #29164,与认领 6573589337 一致;6 个文件 +280/-1,均在申报面内(service-datasource 三个源文件与测试、drivers.mdx、changeset)。
  2. 前提实测:契约复审(CONTRACT_REVIEW_TIER,收窄面)在 a5f6f0c 上 PASS,三条 should-fix 中两条(drivers.mdx 只写了 default、L1404 注释挂在右括号行)由 1384def 修正,第三条(多段字面路由不在推导内)是已知边界,写进 docblock。复审在新 head 上重签 PASS,记录见本 PR 上 head 1384def3 的 Contract review 评论。
  3. 门禁与 dogfood:dev 在 a5f6f0c 跑全量 dogfood,VERDICT 0(389 个文件 / 3215 条测试);新 head 只改文档一段和一行注释位置,不动代码,dogfood 结论沿用。本 head check-runs:38 success、12 按路径 skipped、0 失败。
  4. 接受的偏差:open_question 取 A,只保留与同方法 /:name 路由冲突的名字,不保留 test(它只有 POST /test,没有 POST /:name,今天能正常读写)。
  5. 范围外去向:spec 的 DatasourceSchema.name 仍接受 drivers(代码声明与元数据保存门),已立 spec(data): DatasourceSchema.name accepts drivers (and the metadata save door stores it), a name the datasource admin routes can never serve by name #29308 交 spec 车道。Metadata-protocol: none。
  6. 落地条款:Clause-②: no (narrowing),PR 正文与 changeset 都有;收窄面契约复审已做,全量 dogfood 已做。semver:@objectstack/service-datasource patch,带 BREAKING 说明与 ADR-0087 标记,与 AGENTS.md 一致。check-governed-merges --pr 29262:NOT governed。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 12:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 12:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 4771869 Nov 2, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-0rjenl branch November 2, 2026 12:44
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