Skip to content

fix(runtime): dispatcher capabilities.search answers a stated false, not bare slot presence (#7602) - #7937

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-7602-dispatcher-search-capability
Aug 12, 2026
Merged

fix(runtime): dispatcher capabilities.search answers a stated false, not bare slot presence (#7602)#7937
hotlong merged 1 commit into
mainfrom
claude/issue-7602-dispatcher-search-capability

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7602

What was wrong

getDiscoveryInfo() — the runtime dispatcher's discovery face — derived capabilities.search.enabled from hasSearch = !!searchSvc, i.e. bare service-slot presence.

The defect is not that today's answer is wrong — it is that today's answer is right by accident.false is produced only because nothing fills the slot (CORE_SERVICE_PROVIDER records 'search': null on every host in either repo). Register any ISearchService — which is exactly what the slot exists for, per CoreServiceName's "Search Engine (Elastic/Meili)" — and the discovery document flips search.enabled to true while the host still 404s the endpoint. That is the declared ≠ enforced failure #7541 closed on the getDiscovery() producer, merely waiting on a precondition nobody has met yet.

Premise, verified on origin/main @ 7372d46 before coding

Line numbers had drifted from the card's ~L1111 / ~L1372; located by text.

claimmeasured
the dispatcher mounts no /search routeno search entry in route-ledger.ts, no handler in http-dispatcher.ts, no routes.search key in the emitted routes literal
capabilities.search still slot-derivedhttp-dispatcher.ts:1111const hasSearch = !!searchSvc;:1372search: { enabled: hasSearch }

The card's option 2 (mount a /search dispatcher domain) is deliberately deferred, not overlooked: no route exists and no host registers the slot, so mounting one would be building an unrequested capability.

The change

capabilities.search is now a hardcoded false carrying its reasoning inline, exactly the way websockets immediately below it already is (ADR-0076 D12 / #2462).

The dispatcher's own doctrine at that site says every service whose HTTP surface is a dispatcher domain mirrors that domain's guard — "same predicate ⇒ same answer" (#4000 / #4058). search is not a dispatcher domain, which is precisely why slot presence was never the right predicate for it and why it has no guard to mirror. Should a /search domain ever be mounted, the guard applies and the key is re-derived from it — the comment says so.

Also folds in #5672's Ruling A where it applies: every key from every producer, answered from that producer's own facts. Two producers answering one key by two unrelated predicates was legal only while both answers happened to be honest — this ends that state for search.

services.search is untouched in behaviour, on purpose

The third read the triage comment flagged (search: hasSearch ? svcAvailable(…) : svcUnavailable('search')) stays presence-gated, and correctly so: that map reports what is registered, not what is served — the documented *Registered contract at the same site — and it already passes route: undefined, advertising nothing on the occupant's behalf. hasSearch is renamed searchRegistered and moved into the *Registered group so the two different facts stop sharing one name.

Testing

packages/runtime/src/discovery-schema-conformance.test.ts's existing pin (info.capabilities.search.enabled === false) is not deleted, skipped or weakened — it was correct before this PR and is still green. But it passes for the accidental reason, so this PR adds the case that gives it meaning:

  • new: [#7602] answers 'search' false WITH a search service registered — the slot is not the predicate. It resolves a real-shaped ISearchService occupant, asserts capabilities.search.enabled === false, asserts routes.search stays unadvertised, and re-parses the whole body against DiscoverySchema.
  • anti-vacuity: it first asserts services.search.enabled === true, so the false above is provably a decision about the HTTP surface rather than a fixture that failed to register anything.

Reverse verification (direction predicted before running): with the producer change reverted to search: { enabled: searchRegistered }, the run is 1 failed / 22 passed — the new case red, the pre-existing pin green. That asymmetry is the whole point of the case, and without it this change is untestable.

Gates run

  • node scripts/pm/dispatch-gates.mjs …pnpm check:changeset-gate-self-tests
  • full packages/runtime suite: 139 files / 2128 tests passed
  • full package build closure (turbo run build, 70 tasks) ✅
  • pnpm check:type-check-debt against the built closure ✅ — "none above its recorded number"; @objectstack/runtime measures exactly its recorded 227, i.e. the new test file added zero type errors and no ledger entry was raised.

Wire impact

None on any existing host: the emitted value is false before and after. What changes is that it staysfalse when the slot is filled.

Out of scope, filed separately

services.search reports handlerReady: true for a filled slot with no mounted handler — the same contradiction #4318 fixed for cache/queue/job via svcInProcess (handlerReady: false), left latent on the one slot nobody fills. Not touched here; filed as a finding.


Generated by Claude Code

…e`, not bare slot presence (#7602)
`getDiscoveryInfo()` derived `capabilities.search.enabled` from `!!searchSvc`.
That answer is `false` on every host that exists — but only by coincidence:
nothing registers the `search` slot (`CORE_SERVICE_PROVIDER` records
`'search': null`). The dispatcher mounts no `/search` route at all — no
`route-ledger.ts` entry, no handler, no `routes.search` — so slot presence was
never the right predicate. Register any `ISearchService`, exactly what the slot
exists for, and the discovery document flipped to `true` over an endpoint that
still 404s: the `declared ≠ enforced` defect #7541 closed on the
`getDiscovery()` producer, waiting on a precondition nobody had met yet.
`capabilities.search` is now a hardcoded `false` carrying its reasoning inline,
the way `websockets` immediately below it already is (ADR-0076 D12, #2462). The
dispatcher's own doctrine at that site says a service whose HTTP surface is a
dispatcher domain mirrors that domain's guard — "same predicate ⇒ same answer"
(#4000 / #4058); `search` is not a dispatcher domain, which is precisely why it
had no guard to mirror. This replaces a coincidence with a reason.
`services.search` keeps its presence gate — that map reports what is
REGISTERED, not what is served, and it already advertised no route. `hasSearch`
is renamed `searchRegistered` and moved into the documented `*Registered` group
so the two facts stop sharing one name.
Coverage: `discovery-schema-conformance.test.ts`'s existing pin
(`capabilities.search.enabled === false`) is untouched and still green, but it
passes for the accidental reason. A new case asserts `false` WITH a search
service registered, anti-vacuity-checked against `services.search.enabled ===
true` so the fixture is proven to fill the slot. Reverse-verified: with the
producer change reverted, that one case goes red (1 failed / 22 passed) while
the pre-existing pin stays green.
No wire change on any existing host — the emitted value is `false` before and
after; what changes is that it stays `false` when the slot is filled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeW3KzpmdURCJ32CNTJdM7
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 6:37am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

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

  • content/docs/api/client-sdk.mdx(via packages/runtime)
  • content/docs/api/index.mdx(via @objectstack/runtime)
  • content/docs/api/wire-format.mdx(via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx(via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx(via packages/runtime)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/runtime)
  • 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/vercel.mdx(via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx(via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx(via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx(via packages/runtime)
  • content/docs/permissions/system-context.mdx(via packages/runtime)
  • content/docs/plugins/packages.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/runtime)

2 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/runtime)
  • content/docs/releases/v17.mdx(via @objectstack/runtime)

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.

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.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 12, 2026
@hotlong
hotlong marked this pull request as ready for review August 12, 2026 06:47
@hotlong
hotlong enabled auto-merge August 12, 2026 06:48
@hotlong
hotlong added this pull request to the merge queueAug 12, 2026
Merged via the queue into main with commit 19aaf4bAug 12, 2026
26 checks passed
@hotlong
hotlong deleted the claude/issue-7602-dispatcher-search-capability branch August 12, 2026 06:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding: the runtime dispatcher's capabilities.search is still presence-gated, so it over-promises the moment the slot is filled

2 participants

@hotlong@claude