Skip to content

fix(metadata-protocol,rest,spec): one predicate for capabilities.search and the /search route (#7541) - #7577

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-7541-search-capability-predicate
Aug 11, 2026
Merged

fix(metadata-protocol,rest,spec): one predicate for capabilities.search and the /search route (#7541)#7577
os-zhuang merged 2 commits into
mainfrom
claude/issue-7541-search-capability-predicate

Conversation

@claude

@claude claude Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #7541

/discovery reported capabilities.search = {enabled:false} and services.search = {enabled:false, status:"unavailable"} while GET /api/v1/search?q=audit answered 200 with 5 real hits. Prime Directive #10 inverted: not an advertised endpoint that 404s, but a live endpoint no conforming client will ever call — because the document whose only job is to say what is available said it was not.

Root cause: two producers, two unrelated predicates

predicate where
the advertised bit registeredServices.has('search') getDiscovery(), packages/metadata-protocol/src/protocol.ts
the route's own refusal typeof protocol.searchAll !== 'function'501 registerSearchEndpoints, packages/rest/src/rest-server.ts

Nothing in either repository registers the search slot — the repo's own CORE_SERVICE_PROVIDER table records that as a verified fact — and ObjectStackProtocolImplementation implements searchAll unconditionally. So the two producers did not merely risk disagreeing: they disagreed on every host that exists.

search was the last well-known capability still on bare slot presence. Its neighbours already carry the rule stated in that builder — "the predicate is deliberately the SAME one that decides whether the route is advertised — what we advertise and what we claim cannot disagree" — with chunkedUpload moved onto it in #5672. This brings search onto the same footing: one predicate feeding both the advertised bit and the route's own refusal. No fallback was added to the route.

The two open determinations

1. Does the search service slot carry independent meaning? — Yes. Not collapsed.

Measured before touching it:

  • Registrants: zero. Nothing in this repository calls registerService('search'), and CORE_SERVICE_PROVIDER records 'search': null with the note "Nothing provides the slot at all… Verified across BOTH repositories: nothing in objectstack-ai/cloud registers it" (ADR-0115 Evidence 5).
  • Consumers: one, and it is not the search surface. The only non-discovery reader is http-dispatcher.ts:1080, which resolves the slot purely to fill its own services.search entry. The runtime dispatcher serves no /search domain at all — so no code path anywhere routes a search request through the slot.
  • But the slot is contractual, not noise. CoreServiceName declares it "Search Engine (Elastic/Meili)" and ISearchService is a real index/query contract. A deployment that registers a Meilisearch service to get better search than searchAll's LIKE-scan is exactly what the slot is for.

So the two keys answer different questions, and the schema already says so: WellKnownCapabilitiesSchema.search is "whether the backend supports full-text search" (serveability), while services.search reports which implementation occupies the slot. services.search is therefore left exactly as it was — presence-derived, route /api/v1/search when filled. Collapsing them would have destroyed the slot's only remaining signal.

2. What should services.search.status say? — Still unavailable. The message changed instead.

After the fix an ordinary host reports capabilities.search.enabled: true beside services.search.status: 'unavailable'. Both statements are true — but the old remedy line (No implementation ships for the 'search' slot — register a service under it to enable) reads as "search is dead here", which is exactly how two halves of one document come to look like they contradict each other.

Flipping status to available was rejected: no engine is registered, and saying otherwise is the original defect pointed the other way. Instead the slot gets a REMEDY_DETAIL sentence in @objectstack/specthe treatment ui already carries for the identical shape (#4146: vestigial slot, surface served by the protocol). It keeps the unchanged "no implementation ships" opening (still pinned by core-service-provider.test.ts) and adds which question the entry answers:

No implementation ships for the 'search' slot — a dedicated search engine (Elasticsearch/Meilisearch); register a service under it to enable. Cross-object search does not depend on it: GET {basePath}/search is served by the protocol itself, and whether it is served on this host is reported by capabilities.search — not by this slot.

Because that sentence lives in the one shared helper, the dispatcher and the metadata-protocol builder cannot drift on it.

File surface

File Change
packages/metadata-protocol/src/protocol.ts (@@ -3477) The fix. search: registeredServices.has('search')typeof this.searchAll === 'function' — the route's own 501 predicate.
packages/rest/src/rest-server.ts (@@ -3790) /discovery ANDs that with api.enableSearch, the flag deciding whether this server mounts the route. Same two-layer conjunction transactionalBatch uses with api.enableBatch; the mount's own ?? true spelling is copied verbatim.
packages/spec/src/system/core-services.zod.ts (@@ -97, @@ -131) REMEDY_DETAIL['search'] (determination 2) + the CORE_SERVICE_PROVIDER comment that described null's two cases.
packages/rest/src/discovery-search-capability-agreement.test.ts New pin. Drives the real builder and the real route in one test; asserts agreement.
packages/objectql/src/protocol-discovery.test.ts Two fixtures that asserted search: {enabled:false} — i.e. asserted the defect. Updated and re-commented; one now also pins that the two halves stay independent.
.changeset/search-capability-serveability-predicate.md patch × @objectstack/metadata-protocol, @objectstack/rest, @objectstack/spec.

The pin asserts agreement, not a value

A test that asserts enabled === true passes again the day someone hardcodes the bit. So the new file measures both sides from the real producers in one test — capabilities.search.enabled off the real getDiscovery() through the real /discovery handler, and the status the real registerSearchEndpoints handler actually answers — and asserts declared === served across three hosts that genuinely differ:

host route answers declared agree
ordinary (empty registry, real searchable object) 200, real hits true
api.enableSearch: false (route not mounted) 404 false
protocol without searchAll (instance override) 501 false

A fourth test pins that the three are genuinely discriminated ([true,false,false] / [200,404,501]), so the agreement cannot hold vacuously.

Reverse verification

Predictions were written before any test run.

Test Prediction Measured
pin › ordinary host agrees RED on revert of the metadata-protocol hunk ✅ RED — expected false to be true
pin › enableSearch:false agrees RED on revert of the rest hunk (with the protocol hunk present); GREEN at full baseline → GUARD w.r.t. the whole patch ✅ RED — expected true to be false; ✅ GREEN at full baseline
pin › protocol without searchAll agrees GUARD — green both directions ✅ green both directions
pin › anti-vacuity RED on revert of the metadata-protocol hunk ✅ RED — [false,false,false] vs [true,false,false]
protocol-discovery.test.ts › "capabilities field populated from registered services" RED after the fix — fixture asserted the defect ✅ RED, updated
protocol-discovery.test.ts › "all capabilities false when no services registered" RED after the fix — same cause ✅ RED, updated
protocol-discovery.test.ts › "dynamically set capabilities" (search: true) GREEN both directions — value unchanged, reason changed ✅ green; annotated so its greenness is not read as evidence
protocol-discovery.test.ts › "non-dispatcher-owned routes presence-gated" (services.search.route) GREEN both directions — services.search untouched ✅ green
runtime/discovery-schema-conformance.test.ts:245 (capabilities.search === false) GREEN both directions — dispatcher producer untouched ✅ green
spec/core-service-provider.test.ts (search remedy invariants) GREEN both directions — wording written to keep /No implementation ships/, no Install, contains 'search' ✅ green
rest/discovery-schema-conformance.test.ts (vocabulary + boolean-shape gates) GREEN both directions ✅ green
scripts/check-service-providers.mjs GREEN both directions ✅ green

Missed predictions: none. Two harness corrections were needed before the pin could measure anything, neither of which changed a prediction: sibling packages must be built for @objectstack/metadata-protocol to resolve, and enforceAuth 401s an anonymous request before the searchAll probe — so the pin authenticates, matching step 1 of the issue's reproduction.

Consumption radius

Grepped for every consumer of the capability bit and of the search slot:

  • capabilities.search — no production consumer in this repository. The SDK reads it only through the generic client.capabilities getter (packages/client/src/index.ts); the real consumer is objectui, out of this repo. Test readers: client/client.test.ts (fixture-fed, unaffected), runtime/discovery-schema-conformance.test.ts (dispatcher producer, untouched).
  • the search slot — registrants: none. Consumers: http-dispatcher.ts:1080 (its own services.search entry only). Untouched by this PR.

Two things deliberately left alone, both pre-existing and out of this card's lane:

  1. The runtime dispatcher's capabilities.search stays !!searchSvc. It serves no /search domain, so its honest answer is false — which is what it reports today, since nothing fills the slot. Its predicate is still presence-based and would over-promise if a slot were ever registered on a dispatcher host; that is a separate defect in a third producer, not this seam.
  2. routes.search is still never advertised by either producer (serviceToRouteKey has no search entry), even though SERVICE_CONFIG.search declares the path. The SDK's client.search() uses a fixed path, so nothing is broken by it today.

Gates run

Full package suites, all green (pnpm build first — the earlier run's suite-level failures were unresolved workspace deps, not assertions):

package test files tests
@objectstack/spec 374 9805
@objectstack/metadata-protocol 71 1051
@objectstack/objectql 178 3153
@objectstack/rest 83 1345
@objectstack/runtime 122 1976
@objectstack/client 21 279

17 609 tests, 0 failures.

  • turbo run typecheck over the four touched packages — 20/20 ✅
  • pnpm build — 71/71 ✅
  • node scripts/check-service-providers.mjs
  • node scripts/check-empty-changeset.mjs ✅ · node scripts/check-adr-0087-registration.mjs
  • node scripts/docs-audit/check-audit-scope.mjs ✅ (0 docs affected; content/docs/releases/ untouched)
  • eslint --no-inline-config over every changed file ✅

🤖 Generated with Claude Code

https://claude.ai/code/session_01GHTC2SvMXprxV9eEHqfbCP


Generated by Claude Code

…rch` and the /search route (#7541)

`/discovery` reported `capabilities.search = {enabled:false}` while
`GET /api/v1/search?q=audit` answered 200 with real hits. Prime Directive #10
inverted: a live endpoint no conforming client will ever call, because the
document whose only job is to say what is available said it was not.

Two producers, two unrelated predicates. The bit came from a registered `search`
service slot; the route 501s on `typeof protocol.searchAll !== 'function'`.
Nothing registers that slot in either repository and the protocol implements
`searchAll` unconditionally, so the two did not merely risk disagreeing — they
disagreed on every host that exists.

`search` was the last well-known capability still on bare slot presence. Its
neighbours already carry the rule stated in the builder ("the predicate is
deliberately the SAME one that decides whether the route is advertised"), most
recently `chunkedUpload` in #5672. This puts `search` on that footing:

- metadata-protocol: the bit is now `typeof this.searchAll === 'function'` —
  the route's own refusal predicate.
- rest: `/discovery` ANDs it with `api.enableSearch`, the flag that decides
  whether this server mounts the route at all. Same two-layer conjunction
  `transactionalBatch` uses with `api.enableBatch`. No fallback was added to
  the route.

`services.search` keeps its own answer: the slot is a search ENGINE
(Elastic/Meili, `ISearchService`), so it reports which implementation occupies
it while the capability reports whether the surface is served. Those now
legitimately differ, so spec gives the slot a REMEDY_DETAIL sentence — the
treatment `ui` already carries for the same shape (#4146) — keeping the
unchanged "no implementation ships" fact and adding which question it answers.

The pin drives the real capability builder and the real route in one test and
asserts they agree, over three hosts that genuinely differ (served 200,
unmounted 404, unimplemented 501) — never `enabled === true`, which would pass
again the day someone hardcodes the bit.

Fixes #7541

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

vercel Bot commented Aug 11, 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 Aug 11, 2026 6:44am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/rest, @objectstack/spec.

108 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/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @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/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via 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 @objectstack/metadata-protocol, packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @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/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • 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/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 @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/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 @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest)
  • content/docs/permissions/authorization.mdx (via @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/permissions/system-context.mdx (via packages/rest, packages/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/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/rest, @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/metadata-protocol, @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.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/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.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/field-grouping-and-order.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)

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

  • content/docs/releases/implementation-status.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @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/rest, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol, @objectstack/spec)

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-actions github-actions Bot added documentation Improvements or additions to documentation protocol:system tests tooling labels Aug 11, 2026
…rtial literal

The new test drove both handlers with `{ params, query }` object literals, which
do not satisfy `IHttpRequest` — 2 x TS2345, taking @objectstack/rest's TEST_DEBT
from its recorded 155 to 157. That ledger is a ratchet: it may only shrink.

Fixed at the source rather than by raising the entry, and the fix is not
bookkeeping. `enforceAuth` runs BEFORE either predicate under test and reads
`req.method` and `req.path` — both `undefined` in the old literals — so the pin
was measuring statuses a real caller would not necessarily get. The helper now
returns a complete `IHttpRequest` typed against the contract, so the gate reads
zero errors from this file AND the measured 200/404/501 are the statuses the
real request shape produces.

Not cast to `any`: the neighbouring conformance test casts its handler that way,
but a cast here would have hidden exactly the missing members that make the
measurement faithful.

Measured with the gate's own command (packages/rest/tsconfig.json with the test
globs dropped from `exclude`): 157 before, 155 after — equal to the recorded
ledger entry, which is left untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHTC2SvMXprxV9eEHqfbCP
@os-zhuang
os-zhuang marked this pull request as ready for review August 11, 2026 07:04
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit 69ac82c Aug 11, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7541-search-capability-predicate branch August 11, 2026 07:21
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 protocol:system size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discovery advertises capabilities.search = {enabled:false} while GET /api/v1/search serves 200 with real hits

2 participants