Skip to content

fix(spec,runtime): resolveService 也返回槽的契约,core 槽上的 : any 逃逸清零 (#4127) - #4176

Merged
os-zhuang merged 1 commit into
mainfrom
claude/dispatcher-storage-upload-typeerror-0exbcb
Jul 30, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/dispatcher-storage-upload-typeerror-0exbcb

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Refs #4127。#4127 gate 的第二批(第一批是 #4168)。

#4168 先动 getService,因为它简单 —— 所有调用点本来就传 CoreServiceName。resolveService 是混杂的那个,剩下的 any 都在这里。

1. 重载按证据切开三类

core 名字解析为槽的契约,其余保持 any:

类别 调用点 处理
core 槽,无论怎么写 'metadata'×10、'automation'×3、'auth'×3、'ai' 用裸字符串;另有 ~16 处用 CoreServiceName.enum.* 都解析为契约,调用点零改动
非 core 名字 'protocol'×22、'objectql'×9、'mcp'、'kernel-resolver'、'security'、'scope-manager' 保持 any

同一个槽此前被两种写法寻址('metadata' vs CoreServiceName.enum.metadata),现在两种都受检。

非 core 那些是真实存在的服务,但既不在 CoreServiceName 里、也没有写过契约。它们的 any 就是账本诚实的边界 —— 在这里给它们编一个没有任何东西验证的形状,正是这条工作线要消灭的东西。写它们的契约是另一件事。

2. 真正的发现:类型在三个调用点被抹掉了

const x: any = await deps.resolveService('auth', …) —— 注解赢,#4168 做的一切在那里完全无效。按这个模式扫,core 槽上有三处:

/mcp ×2 —— 又是两个未声明的方法

domain 调的是 authService?.getMcpResourceUrl?.() 和 ?.getMcpResourceMetadataUrl?.()。AuthManager 两个都实现了(auth-manager.ts:2771 / :2796,plugin-auth 内部也在用),IAuthService 一个都没声明。调用点和实现一致,缺的是契约 —— #4127 的标准形状。

但 : any + 可选链的组合让这处比之前几个更糟,不是更好:它让调用既对类型系统不可见、又碰巧安全。方法不存在时返回 undefined,于是 skill 路由静默退回到「从请求 host 推导 MCP URL」—— 也就是说,auth 服务的规范值和推导值之间一旦真的不一致,看起来会和正常运行一模一样。

而 getMcpResourceUrl 存在的全部意义,就是它由 auth 的 basePath 推出,让两者不可能在 API prefix 上分歧 —— 路由自己的注释写着「the auth service owns the canonical value」。

两个都声明为 optional:没有 MCP/OAuth 支持的 auth 提供方合法地填这个槽;而 getMcpResourceMetadataUrl 返回 null(OAuth 轨道关闭 —— 内嵌 AS 未启用,或 origin 不满足 OAuth 2.1 传输规则,fail-closed)与「方法根本不存在」保持语义区分。

/packages ×1

const metadata: any = await deps.getService(…metadata),喂给 new SeedLoaderService(ql, metadata, …)。去掉注解,现在对着 IMetadataService 校验。它旁边的 protocol 和 ql 按上面的理由保留 any。

没有其他 core 槽查找被注解抹掉 —— 这一遍扫描对 domains/*.ts 是穷尽的。

验证

项 结果
@objectstack/runtime 937 tests / 65 files
@objectstack/spec 7112 / 273(auth 契约新增 3 个)
adapter-hono 73
tsc --noEmit spec、runtime、downstream-contract、4 个 examples 全清
pnpm lint clean
9 个 check:* 门禁 全 OK
api-surface.json 无变化 —— 这次加的是 interface 成员,不是新导出

排查记录:notifications.hono.integration.test.ts 本地曾因 Cannot find package '@objectstack/platform-objects' 加载失败,stash 掉全部改动后在干净 main 上同样失败,pnpm install 刷新 workspace 链接后消失 —— 与 #4168 遇到的 plugin-dev 是同一类 worktree 链接问题,与本改动无关。

下一步

剩下的是给 protocol / objectql / mcp / security / shareLinks 这些槽写契约(security 有 ISecurityService 但槽名不在 CoreServiceName 里,是另一种形状的缺口),以及 packages.ts(38 处 as any)和 meta.ts(15 处)里与服务查找无关的那些 as any。

🤖 Generated with Claude Code

https://claude.ai/code/session_015T5CeitwV1jXLvsL1A84tw


Generated by Claude Code

…and the `: any` escapes on core slots are gone (#4127)

Batch 2 of the #4127 gate. #4168 typed `getService` — easy, because every one
of its call sites already passed a `CoreServiceName`. `resolveService` is the
mixed one, and it is where the remaining `any` lived.

Overloads split it exactly where the evidence does. A `CoreServiceName`
resolves to the slot's contract; anything else keeps `any`:

- Core slots, however written. 17 call sites address a core slot with a bare
  literal (`'metadata'` x10, `'automation'` x3, `'auth'` x3, `'ai'`) rather
  than `CoreServiceName.enum.*` — the same slot addressed two ways. Both
  resolve to the contract now, with no edit to the call sites.
- Everything else — `protocol` (x22), `objectql` (x9), `mcp`,
  `kernel-resolver`, `security`, `scope-manager`. Real services with no
  `CoreServiceName` entry and no written contract. They keep `any` rather than
  being given a shape here that nothing verifies: that `any` is where the
  ledger honestly ends, and writing those contracts is its own change.

The typing was being erased at three call sites, and that is the actual
finding. `const x: any = await deps.resolveService('auth', ...)` defeats all of
this — the annotation wins and #4168's work does nothing there. Sweeping for
the pattern found three on core slots:

`/mcp` x2 — two more undeclared methods. The domain calls
`authService?.getMcpResourceUrl?.()` and `?.getMcpResourceMetadataUrl?.()`.
AuthManager implements both (plugin-auth uses them internally); IAuthService
declared neither. Call site and implementation agree, the contract is the thing
nobody wrote.

The `: any` + optional-chaining combination made this WORSE than the earlier
gaps, not better: invisible to the type system AND accidentally safe. An absent
method returns `undefined`, so the skill route silently fell back to deriving an
MCP URL from the request host — a real disagreement between the auth service's
canonical value and the derived one would have looked like normal operation. The
whole point of `getMcpResourceUrl` is that it comes off the auth `basePath` so
the two CANNOT disagree about the API prefix; the route's own comment says "the
auth service owns the canonical value".

Both declared optional: an auth provider without MCP/OAuth support fills the
slot legitimately, and `getMcpResourceMetadataUrl` returning `null` (OAuth track
off) stays distinct from the method being absent.

`/packages` x1 — `const metadata: any = await deps.getService(...metadata)`,
feeding `new SeedLoaderService(ql, metadata, ...)`. Annotation dropped; it
typechecks against IMetadataService now. Its neighbours `protocol` and `ql` keep
their `any` for the honest reason above.

No other core-slot lookup is annotated away — the sweep is exhaustive over
domains/*.ts. `api-surface.json` is unchanged: the two additions are interface
MEMBERS, not new exports.

Refs #4127

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015T5CeitwV1jXLvsL1A84tw
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 1:56pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/spec.

113 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/runtime, packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 14:10
@os-zhuang
os-zhuang merged commit 2cb6d3c into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/dispatcher-storage-upload-typeerror-0exbcb branch July 30, 2026 14:10
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…Name` (#4127) (#4202)

Batch 3 of the #4127 gate, after #4168 (`getService`) and #4176
(`resolveService`).

Three slots — `security`, `shareLinks`, `objectql` — each had a written
contract, a provider registering them, and call sites already inside the
contract. The only missing link was that the slot name was not a
`CoreServiceName` member, so nothing could connect them and all three sat
behind `as any`.

The ledger extends past the enum rather than the enum growing. The two answer
different questions, and conflating them is what left these untyped:
`CoreServiceName` answers "what happens at boot when this slot is empty?" — it
sits beside `ServiceCriticality` and drives startup orchestration and
discovery, so adding a member changes runtime behaviour and is effectively
permanent. The ledger answers "what shape occupies this slot?" — pure type
information. These three need only the second, so `ServiceSlotContracts extends
CoreServiceContracts` adds them there and `resolveService` keys on `keyof
ServiceSlotContracts`. Zero runtime effect. If one is later promoted to a
genuine core service, its entry moves up and nothing else changes; a test
asserts these keys are NOT enum members, so that migration cannot happen
silently.

Evidence before an entry, as always: `plugin-security` registers `security` and
`ISecurityService`'s own doc names that registration; `plugin-sharing`
registers `ShareLinkService`, which declares `implements IShareLinkService`;
and `objectql` is an ALIAS of `data` — `packages/objectql`'s plugin registers
the same instance under both names two lines apart, so one object was resolving
as `IDataEngine` through one name and `any` through the other. `protocol` (22
call sites) and `mcp` have no written contract and stay unmapped.

Turning it on found four things, all on the `/security` admin surface:

1. Request input reached the security service unvalidated. `?status=` was
   `String(query.status)` — any string — handed to a method whose contract
   declares exactly three values, and from there into the query's `where`
   clause. Not an injection (the `where` is structured, never interpolated),
   but `?status=garbage` matched no row and returned an empty list, which reads
   as "there are no suggestions" rather than "that is not a status". Now a 400.

2. A test pinned that bug as expected behaviour. The existing case asserted
   `status: 'open'` — not one of the three declared values — reached the
   service and returned 200. It proved the delegate carried A FILTER and
   nothing about that filter being a status. Same shape as batch 1's
   `auth.handler` mocks: coverage in appearance, a wrong contract in substance.

3. and 4. Two writes could not prove they had a caller.
   `confirmAudienceBindingSuggestion`/`dismissAudienceBindingSuggestion`
   declare `callerContext: SecurityContext` non-optionally — deliberately,
   since the read beside them declares it optional — and the domain passed a
   possibly-`undefined` execution context.

   This was NOT a live hole, and the distinction matters: with no execution
   context `shouldDenyAnonymous` already denied, because it sees no
   `userId`/`isSystem` and its allowlist arm needs a non-empty `path` this seam
   never passes, so it fell through to `return true`. What it never did was
   narrow `ec` itself — it only read `ec?.userId`. Checking `ec` directly is
   behaviour-preserving and makes the invariant legible to the compiler and the
   next reader.

The `?status=` rejection is the one BEHAVIOUR CHANGE: an unknown status was a
silent empty list and is now a 400 naming the accepted values. The accepted set
is a `Record` keyed on the contract type, so adding a status to the contract
leaves a key missing and renaming one leaves a key excess — either fails to
compile, where a plain array would have drifted silently. Documented on the
permission-sets page, where the endpoint is described.

Refs #4127
os-zhuang added a commit that referenced this pull request Jul 30, 2026
… which found the project-membership gate not gating (#4127) (#4214)

Batch 4 of the #4127 gate. #4168/#4176/#4202 made a slot lookup return the
slot's contract. Nothing protected that: an `any` annotation on the RESULT
switches the checking back off for that call site, silently, with no test
failing and no visual difference from code that has it. Three such sites already
existed and were found by grep — the same unrepeatable sweep this work replaced.

The rule bans `: any` / `as any` on a `resolveService` / `getService` /
`getRequestKernelService` result. Slots with no written contract (`protocol`,
`mcp`, `kernel-resolver`, `scope-manager`) are exempted BY NAME, CENTRALLY, in
eslint.config.mjs — not by inline disables, because `pnpm lint` runs
`--no-inline-config` and ignores those on purpose. The effect is the one worth
having: a deliberate gap is a reviewed line in one file, a careless one is a
build failure, and they stop looking identical in the code.

ITS FIRST RUN FOUND A LIVE FAIL-OPEN. `enforceProjectMembership` read the
session as `authService?.api?.getSession?.(...)` with no `getApi()` fallback —
the only one of the codebase's three `.api` readers without it. `plugin-auth`
registers `AuthManager`, which has NO `.api` member at all. So the read yielded
`undefined`, `userId` stayed unset, and the function returned at its "anonymous
— upstream auth will decide" line BEFORE ever querying `sys_environment_member`.
A signed-in non-member passed the gate, on every deployment with project scoping
on — which is where the flag defaults to true. Anonymous callers were still
denied elsewhere (#2567/#3963), so this was specifically the signed-in
non-member case.

The existing test for that gate mocked auth as `{ api: { getSession } }` — the
legacy shape the shipped provider does not have — so it was green throughout.
That is the FOURTH test in this work line found encoding a contract nobody
implements, after batch 1's three `auth.handler` mocks and batch 3's
`status: 'open'`. The new test uses the `getApi()` shape and fails against the
pre-fix code.

Also found by the rule, all the same #4127 shape (implemented, called,
undeclared) and all now declared: IAuthService gains `api`, `getApi`,
`isAuthGateActive` and `verifyMcpAccessToken`; IMetadataService gains `load` and
`loadDiagnosed`. `getApi`'s return type is the EVIDENCED SUBSET —
`getSession({headers})` and the three fields callers read — not a
re-declaration of better-auth's handle, which belongs to that library.

And the pattern's real root: the lookup facade returning `any` was re-declared
in THREE places. Batches 1-3 typed `DomainHandlerDeps` and left
`ActionExecutionDeps` and resolve-execution-context's `ResolveOptions` still
saying `any` — so the copy that stayed untyped was the way around all the
others, and it is where the auth reads lived. All three are typed now.

Completing the interface: `getRequestKernelService` gets the same overload split
(its one caller resolves the same `objectql` slot the `resolveService` fallback
beside it does, so the two arms of one expression had different types), and
share-links' `getEngine` loses a `Promise<any>` return annotation — a THIRD
erasure syntax after `: any` and `as any`, and one this AST rule cannot see.
That residual is documented in the config.

`getObjectQL` STAYS `any`, deliberately, with the reason recorded: it exists to
reach ObjectQL's surface beyond IDataEngine (`registry`, `executeAction`), which
has no contract. Typing it IDataEngine would be the comfortable-looking lie.

The auth and metadata contract doc pages are brought back in step with the
interfaces — the auth page still called itself "intentionally minimal" while
missing six members, two of them from batch 2.

Refs #4127
os-zhuang pushed a commit that referenced this pull request Aug 4, 2026
…ct, not `any` (#5081)

The ADR-0120 D5e advisory's `SchemaStack.allObjects()` erased its
service-lookup result to `any`, which `no-restricted-syntax` rejects
(#4168/#4176/#4251): the lookup already returns the slot's contract, and
the annotation switched that checking off while looking identical to code
that has it.

The `objectql` slot HAS a contract — `IObjectQLEngine`, whose
`registry.getAllObjects()` is exactly what this reads — so this takes the
prescription's first branch (pass the contract type) rather than the
UNCONTRACTED_SLOTS registration. Concretely it buys what the rule is for: a
rename of `registry` or `getAllObjects` now breaks this at compile time
instead of silently returning zero objects and turning the advisory mute.

`allObjects()` returns `unknown[]` accordingly; its only consumer,
`collectGlobalUniques`, already accepts `unknown`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…装期姿态硬门 + D5c advisory + 成文契约扫荡 + 三姿态 conformance (objectstack-ai#5081) (objectstack-ai#5314)

* feat(types,cloud-connection,lint,cli): ADR-0120 17.x closeout — isolated install gate, D5c advisory, truth sweep, three-posture conformance (objectstack-ai#5081)

ADR-0120's third and final 17.x block, on top of objectstack-ai#5212 (driver D3+D4) and
objectstack-ai#5208 (spec vocabulary + D5a/D5b lint).

D5e — the isolated-posture install gate. Installing an app that carries
'global' uniques on its own (non-sys) objects into an isolated environment
now stops and lists each index; the installer confirms them as genuinely
platform-wide or rewrites them to 'organization'. The confirmation is
recorded in the install manifest ADR-0104 attestation style (what, who,
when, under which posture) and is never re-asked. A stopped install
registers nothing and writes no ledger entry. NEVER a boot-time warning
(objectstack-ai#4884): rehydrate does not evaluate the gate, and the two populations the
gate cannot reach — pre-gate installs, post-install posture changes — are
covered by the advisory form in `os doctor` / `os migrate plan`.

The pure enumerator lives in @objectstack/types so the hard stop and both
advisories read one classification. Declared-index bare `unique: true`
counts (D1 makes it the positional spelling of 'global'); field-level
`true` does not; sys_/base_ objects do not.

D5c — new advisory rule `unique/legacy-organization-composite` for the S6
hand-written organization composite, pointing at the 'organization'
respelling that closes its NULL hole (objectstack-ai#5030). Advisory forever, never
auto-fixed: opting in is a real D4 tightening.

D6 — indexing.mdx §Two ways to say "unique" rewritten in the new
vocabulary; schema.mdx §Uniqueness and tenancy rewritten as §Uniqueness and
scope, replacing the "composite degenerates to the single-column one" claim
objectstack-ai#5030 falsified; cli.mdx drift-op table updated; references regenerated via
gen:schema && gen:docs; skills/objectstack-data swept (and its non-existent
`tenant_id` column corrected to organization_id).

Conformance — one fixture app booted under single | group | isolated, each
S-row's enforcement asserted with real violating inserts, the materialized
unique key parts asserted byte-identical across all three, plus the one
transition smoke the ADR asks for: a posture flip emits zero drift ops.

Fixes objectstack-ai#5081

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB

* fix(cli): read the `objectql` slot through its IObjectQLEngine contract, not `any` (objectstack-ai#5081)

The ADR-0120 D5e advisory's `SchemaStack.allObjects()` erased its
service-lookup result to `any`, which `no-restricted-syntax` rejects
(objectstack-ai#4168/objectstack-ai#4176/objectstack-ai#4251): the lookup already returns the slot's contract, and
the annotation switched that checking off while looking identical to code
that has it.

The `objectql` slot HAS a contract — `IObjectQLEngine`, whose
`registry.getAllObjects()` is exactly what this reads — so this takes the
prescription's first branch (pass the contract type) rather than the
UNCONTRACTED_SLOTS registration. Concretely it buys what the rule is for: a
rename of `registry` or `getAllObjects` now breaks this at compile time
instead of silently returning zero objects and turning the advisory mute.

`allObjects()` returns `unknown[]` accordingly; its only consumer,
`collectGlobalUniques`, already accepts `unknown`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… commits that decided them (objectstack-ai#20693)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the fifth stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/services/service-datasource/src/**` and nothing else. By the
seat's fresh census at the claim (`5894843429`), it is the largest
package in the lane that no in-flight work holds. Later stages cover the
other packages, so this PR says `Part of` and the card stays open.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), by the method of stages 1 to 4 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`). That is **75 sites on 74 lines in 23 files,
covering 16 numbers**:

- 44 census sites (every census site this package has);
- 31 sites in test comments, which the census defers.

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **17 distinct shas**. `objectstack-ai#8696` was one card fixed in two halves,
so its lines cite the half they describe: the mysql DSN branch
(`72050cc47`) or the mongodb DSN branch (`90a12fb18`). `PR objectstack-ai#8588` was
itself a pull request, and it now cites its squash commit `3dede582b`.
No number in this package has an ADR or ruling record of its own in the
repository (a grep of `docs/adr/` for all 16 finds none), so every
anchor is a commit, per ruling C's order. No number was dropped.

Only comments changed. Every touched source file keeps its line count
(78 lines out, 78 in, over 23 files), so no line citation into these
files moves. 4 of those 78 lines hold no dead citation; they are reflow,
listed under Wordings below. No code token moves (see the guard below).

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces: the only one is `objectstack-ai#12482`, which
resolves and stood on `datasource-connection-service.ts:101` before.
Over the whole diff, added minus removed is 0 or negative for every
number, and no number is new to the diff. No PR number stands on an
added line.

Thirteen dead sites are left on purpose, all of them test titles (see
the list below).

One more file: a `patch` changeset for
`@objectstack/service-datasource`, because the rewritten docblocks ship
(see Changeset below).

## Census: `service-datasource`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count below is its `allocated-but-absent` findings under
`packages/services/service-datasource/`. Each run counts as a reading
only because its board frontier equals the newest issue number, read by
a separate request just before and just after the run.

| reading | tree | board | whole-repo `allocated-but-absent` |
service-datasource sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `6981abfd2`, run 2026-09-29T17:02:34Z to 17:06:27Z |
enumerated, 186 pages, frontier objectstack-ai#20684 (newest objectstack-ai#20684 before and after),
18,511 numbers | 1,318 | **44** | 43 | 9 | 14 |
| after | head `f5ec6bacd`, run 17:18:37Z to 17:22:33Z | enumerated, 186
pages, frontier objectstack-ai#20686 (newest objectstack-ai#20686 before and after), 18,513 numbers
| 1,274 | **0** | 0 | 0 | 0 |

The before count matches the seat's census at the claim (44 sites in 9
files, at `6bff748b`). The whole-repo drop is 44, exactly this diff's
census sites. The `resolves` tally is 32,909 in both runs, and
`resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (994) did
not move either. The after run was taken on `f5ec6bacd`; the head
`265dc6861` adds only the changeset. No run was truncated or discarded:
all four enumerations in this stage (two census runs and the two
supplementary boards below) read 186 pages at the newest frontier.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes test comments. So
a second reading runs the gate's own exported `extractCitations`
(whole-file and comment-prose projections) and `classifyCitation` over
every `.ts` file under `service-datasource/src` (61 files). It uses one
board for both trees, enumerated by the gate's own `enumerateBoard` at
17:22:42Z (186 pages, frontier objectstack-ai#20686, equal to the newest).

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `6981abfd2` | 791 | **88** | 44 | 31 | 0 | 13 |
| after, `f5ec6bacd` | 716 | **13** | 0 | 0 | 0 | 13 |

Its src-comment column equals the census's 44, which is the control on
the second instrument. The 646 resolving and 57 pull-request citations
are the same in both readings, and the drop of 75 citations is exactly
the rewritten sites. An earlier board (17:07:08Z, frontier objectstack-ai#20685) gave
the same base reading. A third, raw reading (every `#` followed by
digits, judged against the same board, whatever surrounds it) finds 88
dead occurrences before and 13 after, and its residue equals the gate's
residue site for site.

## Per-number table

Sites and files count every dead occurrence in scope at the base
(comments and strings, tests included). `rewritten / left` counts the
sites rewritten and the sites left. Each anchor was read in its message
and diff, not only its subject.

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#6268` | 6/4 | 6/0 | `68f5eccb1`: the libSQL/Turso host loader gets
one owner (`@objectstack/runtime`), and `MissingDriverPackageError`
becomes one class across both hosts, because `serve.ts` decides fatality
with `instanceof`. The cli and runtime stages' anchor |
| `objectstack-ai#6345` | 9/5 | 9/0 | `e2798fab7`: one driver vocabulary; `mongo`
renamed to `mongodb`, `turso` made a full builtin with a contract, and
this factory's dispatch made exhaustive. The spec stages' anchor |
| `objectstack-ai#8588` | 1/1 | 1/0 | `3dede582b`: `external.credentialsRef` (and only
it) allowed on `schemaMode: 'managed'`; `objectstack-ai#8588` was that pull request,
and this is its squash commit |
| `objectstack-ai#8696` | 13/4 | 10/3 | two halves: `72050cc47`, a bound
`credentialsRef` reaches the mysql client on the DSN branch as `{ uri,
password }` (5 sites); `90a12fb18`, the mongodb DSN branch carries it in
`options.auth` beside an unmodified url (5 sites). The spec stage's
anchor for the mongo half |
| `objectstack-ai#8873` | 8/3 | 7/1 | `096106522`: a bound `credentialsRef` reaches
the postgres SERVER on the DSN branch; `connectionString` is dropped and
`pg` gets its own parse of the url with the credential attached. The
spec stage's anchor |
| `objectstack-ai#8874` | 9/2 | 7/2 | `d70428ae7`: a declared `ssl` reaches the mysql
client on both branches, in the spelling `mysql2` accepts (`{}`, never
`true`). The spec stage's anchor |
| `objectstack-ai#8876` | 1/1 | 1/0 | `d634e665b`: `urlUserinfoUsername` exported, the
username half of the shared URL userinfo grammar. The spec stage's
anchor |
| `objectstack-ai#9040` | 4/3 | 2/2 | `24206416a`: a credential in the mongo options
passthrough (`config.options.auth.password`) is refused at publish. The
spec stage's anchor |
| `objectstack-ai#9041` | 2/2 | 2/0 | `d491625c1`: a bound `credentialsRef` with a
user-less mongo `config.url` is refused at the one door that sees both
halves. The spec stage's anchor |
| `objectstack-ai#10537` | 3/2 | 3/0 | `e634ecf6a`: `POST /external/validate` scoped
to the URL's datasource; it adds `validateDatasource`. The rest and
runtime stages' anchor |
| `objectstack-ai#10962` | 5/2 | 4/1 | `29d067646`: one live introspection per
datasource per validation sweep, memoised per call and never per
instance (its message names `objectstack-ai#10962`) |
| `objectstack-ai#11166` | 5/1 | 4/1 | `735f5c709`: an unreachable remote is the new
`unreachable` diff kind, not `missing_table`. The runtime stage's anchor
|
| `objectstack-ai#12010` | 9/5 | 8/1 | `77b91bdb4`: `ConnectionEngineLike` derived
from the engine contract, and `registerDriver` stops promising it
accepts any value. The runtime stage's anchor |
| `objectstack-ai#12248` | 1/1 | 1/0 | `8425c17cc`: the ruled engine members adopted
onto `IDataEngine`, the datasource-lifecycle trio among them. The spec
stage's anchor |
| `objectstack-ai#12943` | 3/2 | 3/0 | `090f2302e`: the guarded optional-driver loads
declared as optional peers of this package. The cli and runtime stages'
anchor |
| `objectstack-ai#13279` | 9/4 | 7/2 | `6a180e42d`: a failed permission-store read
raises `AuthzStoreUnavailableError` (503) instead of reading as zero
grants; its message carries the 2026-08-30 ruling, and it moved
`driver-error-classification.ts` into `@objectstack/types`. The anchor
of stage 2 and of the rest, runtime and types stages |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 17), and every one is an
ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 17; the
history is complete, `--is-shallow-repository` false, 15,110 commits).
Where an earlier stage already anchored a number, this stage reuses that
anchor after checking it against this package's lines. New to the sweep
here: `72050cc47` (the mysql half of `objectstack-ai#8696`), `3dede582b` and
`29d067646`.

## Wordings to check

- **`objectstack-ai#8696`'s two halves.** The mysql arm's lines
(`default-datasource-driver-factory.ts:718`, `:824`, and
`bound-secret-dsn-branches.test.ts:4`, `mysql-dsn-ssl.test.ts:189`,
`:263`) cite `72050cc47`; the mongo arm's lines
(`default-datasource-driver-factory.ts:926`, `:954`, `:1243`,
`datasource-credential-migration.ts:182`, and the heading
`bound-secret-dsn-branches.test.ts:72`, 「the mongodb half, added
second」) cite `90a12fb18`.
- **A referent, `datasource-connection-service.ts:95-96`.** 「the
inventory that filed that card」 lost its referent with the number, so it
now says 「the inventory that filed its card」, the card behind
`77b91bdb4` (1 reflow line).
- **`datasource-connection-service.ts:100-101`.** 「objectstack-ai#12248 adjudicated
all three onto IDataEngine」 became 「Commit 8425c17 adopted all three
onto IDataEngine per the ruling」: the ruling decided and the commit
carried it out, as its changeset says (1 reflow line).
- **What the card described, `default-datasource-driver-factory.ts:412`
and `mysql-dsn-ssl.test.ts:40`.** 「the one objectstack-ai#8874 describes as honouring」
became 「the one commit d70428a's card describes as honouring」, since
the words quote the card, not the commit.
- **A cross-reference, `default-datasource-driver-factory.ts:736`.**
「the falsy-value note under objectstack-ai#8874 below」 points at the heading at
`:767`, which now carries `commit d70428a`, so the pointer names the
same anchor.
- **A future tense made past, `postgres-dsn-bound-secret.test.ts:223`.**
「the authoring door (objectstack-ai#9041), which this card lands before」 became
「(commit d491625), which landed after this pin」. `096106522` (this
file's commit) is an ancestor of `d491625c1`, both on 2026-08-16.
- **`external-datasource-service.test.ts:444`.** 「The card's measured
defect」 became 「Its card's measured defect」, the card behind
`735f5c709`; `:588` 「the pre-objectstack-ai#10537 route」 became 「the route … before
commit e634ecf」.
- **`datasource-admin-service.test.ts:674`.** 「Before PR objectstack-ai#8588」 became
「Before commit 3dede58」, the squash commit of that pull request, which
answers 404.
- **Reflow, 4 lines with no dead site** (every file keeps its line
count): `datasource-connection-service.ts:96`, `:101`,
`default-datasource-driver-factory.ts:825`, `:826`.

## The 13 sites left

- **Test titles, 13 sites.** `describe` / `it` titles, which are string
tokens, left as stages 1 to 4 left theirs:
`admin-routes-authz-outage-envelope.test.ts:158` (`objectstack-ai#13279`);
`admin-routes-tenancy-posture-admission.test.ts:557` (`objectstack-ai#13279`);
`bound-secret-dsn-branches.test.ts:136`, `:244` (`objectstack-ai#8696`);
`connection-engine-like-contract.test.ts:21` (`objectstack-ai#12010`);
`datasource-config-redaction.test.ts:406` (`objectstack-ai#9040`);
`datasource-credential-migration.test.ts:226` (`objectstack-ai#9040`);
`external-datasource-service.test.ts:453` (`objectstack-ai#11166`), `:690` (`objectstack-ai#10962`);
`mysql-dsn-ssl.test.ts:165`, `:324` (`objectstack-ai#8874`), `:260` (`objectstack-ai#8696`);
`postgres-dsn-bound-secret.test.ts:160` (`objectstack-ai#8873`).
- There is no operator string, assertion message, generated header or
quoted ruling carrying a dead number in this package. The verbatim
maintainer quotations in scope (「同意」 and 「同意所有」, on 8 lines) carry no
dead number and are untouched.

## Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes, with comments as
trivia and JSDoc nodes never visited, base `6981abfd2` against head.
Template literals are therefore read in context. It ran over all 23
touched `.ts` files.

- Real run: 24,055 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `default-datasource-driver-factory.ts` (`Lazy +
caught exactly like` to `Lazy and caught exactly like`): 0 files
changed, as expected (exit 0).
- Positive control, a code token added in
`default-datasource-driver-factory.ts` (`const url =
resolveTursoUrl(spec);` given a trailing `?? undefined`): DIFFER (exit
1).
- Positive control, one digit changed inside a kept test title
(`mysql-dsn-ssl.test.ts:165`): DIFFER (exit 1).

Every mutation went through `scripts/ablation-replace.mjs`, and each
landed (anchor 1 to 0, blob changed). Each restore was proven
byte-identical to the HEAD blob (`8c164aa6178f`, `2feeaeb0411d`), with
`git diff HEAD` empty and a clean tree afterwards. A first draft of the
guard used the bare scanner, which loses template context and reported
token changes inside comments; it was replaced by the parser walk before
any reading was taken from it.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/service-datasource`
(`.changeset/20596-service-datasource-provenance-anchors.md`) is
included. It says only that the provenance comments were re-anchored, in
stages 3 and 4's words.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build, the rewritten comments reach `dist`:
`6a180e42d`, `e2798fab7` and `e634ecf6a` once, and `29d067646` three
times, in each of `dist/index.d.ts`, `index.d.cts`, `index.js` and
`index.cjs`; `68f5eccb1` 4 times, `090f2302e` twice, and `77b91bdb4` and
`8425c17cc` once each, in both declaration files. Positive control: the
unchanged line 「`registerDatasourceDef`, `markDatasourceUnavailable`,」
beside the shipped rewrite at `datasource-connection-service.ts:95` is
found once in `index.d.ts`. A never-written negative phrase appears
nowhere in `dist`. No dead number of the 16 is left anywhere in `dist`.

## Gates (head `265dc6861`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
(self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0:
the diff-scoped run judged 1 citation (`objectstack-ai#12482`), and it resolves.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0, with the
sibling-package prose ids at their baseline and no growth.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `265dc6861` derived 63 commands:
all 54 derived at dispatch, plus `check:duration-unit-keys`,
`check:dispatcher-error-vocabulary`, `check:engine-double-contract`,
`check:logger-receiver-detach`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:type-check-coverage`,
`check:type-check-debt` and `check:where-matcher`. Each ran with its
exit code captured before any pipe, and all 63 exit 0. `--ran`, fed each
command with its exit code, reports 63 run, 0 NOT MEASURED (a derived
zero), 0 unrun, and exits 0. A full `turbo run build` of `./packages/*`
and `./packages/*/*` ran first under the shared verify lock (71 of 71
tasks, exit 0), so no gate hit an unbuilt workspace.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/service-datasource test`: 34 files pass
and 693 tests pass. That is every test file in the package, the 14
touched ones included.
- `pnpm --filter @objectstack/service-datasource typecheck` exits 0. Its
`tsconfig.json` includes all of `src`, and `--listFiles` shows all 61
files under `src/`, the 34 test files included, and all 23 touched files
in the program.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 23 touched `.ts` files gives 23 files, 0 errors and 0
warnings. All 23 are in eslint's own population (`isPathIgnored` is
false for each). `eslint.config.mjs` never enables type-aware linting
(no `parserOptions.project`, as its own lines 327-328 state), so a
comment edit here cannot move the verdict on any untouched file. The
repo-wide `pnpm lint` is CI's run.
- **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of
the 24 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636). In this package there is no `#N-word` spelling at all. There
are 7 `#A/#B` lines (`admin-routes.ts:28`,
`datasource-route-ledger.ts:159`, `turso-driver-config.ts:132`,
`external-introspection-seam.test.ts:14`, `:102`, `:163`,
`turso-bound-secret-authoring.test.ts:8`), and every second number on
them is live: `objectstack-ai#10998`, `objectstack-ai#4251` and `objectstack-ai#4249` are issues, and `objectstack-ai#8078`,
`objectstack-ai#4176` and `objectstack-ai#4202` are pull requests. So nothing there needed
rewriting. The raw scan above, which sees both spellings, agrees.
- **The census instrument did not truncate in this stage.** Four
enumerations read 186 pages each at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#13279` →
`6a180e42d`; `objectstack-ai#12010` → `77b91bdb4`; `objectstack-ai#6345` → `e2798fab7`; `objectstack-ai#6268` →
`68f5eccb1`; `objectstack-ai#12943` → `090f2302e`; `objectstack-ai#8696` → `72050cc47` (mysql) or
`90a12fb18` (mongodb); `objectstack-ai#8873` → `096106522`; `objectstack-ai#8874` → `d70428ae7`;
`objectstack-ai#10962` → `29d067646`.
- **Base.** The branch is 5 commits behind `origin/main` (`14f80e239`,
read at 17:27Z). None touches `service-datasource`, `scripts/` or
`.changeset/config.json`, so there was no merge.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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

Development

Successfully merging this pull request may close these issues.

2 participants