Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .changeset/remove-dead-sdk-surface.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
"@objectstack/client": major
---

feat(client)!: remove the `ai` namespace — three methods, none of which ever worked (#3718)

`client.ai` held exactly three methods, and **no server in any repo has ever
mounted the URLs they build**:

| Removed | Built | Why it 404ed |
|---|---|---|
| `client.ai.nlq` | `POST /api/v1/ai/nlq` | declared in `DEFAULT_AI_ROUTES`, which has no runtime consumer — only the spec's own test reads it; `aiNlq?` is an optional protocol method nothing implements |
| `client.ai.suggest` | `POST /api/v1/ai/suggest` | same |
| `client.ai.insights` | `POST /api/v1/ai/insights` | same |

Found by the AI route ledger (#3718, in `cloud`, where `service-ai` lives),
which enumerates the table `buildAIRoutes()` returns and matches the SDK's URLs
against it. The two sets are **disjoint**: the real AI surface is 12 routes —
`chat`, `chat/stream`, `complete`, `models`, `status`, `effective-model` and six
`conversations` routes — and the SDK expressed none of them.

**Removed, not deprecated.** A typed method that always throws is worse than no
method: it costs a runtime round-trip to discover, where absence is a compile
error. No working code can break, because there was no working behaviour. This
lands in the v17 major `@objectstack/client` is already taking, which is the
right window for a breaking removal rather than a reason to defer one.

Expressing the real surface is tracked on #3718 as **new** API, not a rename of
what was removed. For chat, `useChat()` (`@ai-sdk/react`) already speaks the
Data Stream Protocol `POST /api/v1/ai/chat` serves.

Also removed: the `AI_PLANE` exemption added to the capstone hours earlier
(#3727). With no method targeting `/api/v1/ai/`, an exemption there is a hole
with nothing to cover — the wildcard-only bound stays `0` and now reaches 0
with nothing exempted to get there.

The four AI tests in `client.test.ts` are **replaced, not deleted**. They were
the exact shape this audit keeps finding behind green suites: mock `fetch`,
assert the URL the client *built*, never assert that anything answered it. They
passed for years against three endpoints that did not exist. The replacement
asserts the one thing worth defending — the namespace is gone and must not
return without a route behind it.

`Ai{Nlq,Suggest,Insights}{Request,Response}` are still re-exported straight
from `@objectstack/spec/api`, so anyone holding those types keeps them.
Retiring the spec-side declarations is a separate change.

Docs corrected: `client-sdk.mdx` carried three copy-pasteable examples that
404ed, and `plugin-endpoints.mdx` had the AI surface **inverted** — it tabled
the three phantom routes and explicitly denied `/ai/chat`, which is mounted. It
now lists the 12 real ones.
27 changes: 15 additions & 12 deletions content/docs/api/client-sdk.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -312,18 +312,21 @@ await client.notifications.markAllRead();
// through sys_inbox_message and tracks read-state in sys_notification_receipt.
// These helpers will be repointed during the objectui bell cut-over.

// AI — DECLARED BUT NOT IMPLEMENTED (#3718). These three methods build
// /api/v1/ai/{nlq,suggest,insights}, and no server in any repo mounts those
// paths, so every call 404s. They are typed and shipped, which is why they
// look usable; they are not. Do not build on them until #3718 resolves.
await client.ai.nlq({ query: 'Show me all active accounts' });
await client.ai.suggest({ object: 'account', field: 'industry' });
await client.ai.insights({ object: 'sales', recordId: dealId });
// Conversational chat is not on the client — use the Vercel AI SDK
// (`useChat()` from `@ai-sdk/react`) directly against the chat endpoint.
// That endpoint DOES exist (POST /api/v1/ai/chat, plus /chat/stream,
// /complete, /models and six /conversations routes, all served by service-ai);
// the SDK simply has no method for any of them.
// AI — the `client.ai` namespace was REMOVED in v17 (#3718).
//
// It held `nlq`, `suggest` and `insights`, which built
// /api/v1/ai/{nlq,suggest,insights}. No server in any repo ever mounted those
// paths, so every call 404ed for the whole life of the namespace. They were
// typed and shipped, which is exactly why they looked usable.
//
// The AI surface that DOES exist is served by `service-ai` (Cloud/EE):
// POST /api/v1/ai/chat and /chat/stream, POST /complete, GET /models, and six
// /conversations routes. The SDK has no method for any of them yet — that is
// tracked on #3718 as new API, not as a rename of what was removed.
//
// For chat, call the endpoint directly with the Vercel AI SDK
// (`useChat()` from `@ai-sdk/react`); it speaks the Data Stream Protocol that
// POST /api/v1/ai/chat serves.

// i18n — Internationalization
await client.i18n.getLocales();
Expand Down
54 changes: 25 additions & 29 deletions content/docs/api/plugin-endpoints.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -95,37 +95,33 @@ The core dispatcher implements only the list / read / read-all routes above. Dev

### AI (`/ai`) — Plugin Required

| Method | Endpoint | Description | Status |
|:-------|:---------|:------------|:-------|
| POST | `/ai/nlq` | Natural language → query | **Not implemented** |
| POST | `/ai/suggest` | Get value suggestions | **Not implemented** |
| POST | `/ai/insights` | Get data insights | **Not implemented** |
These are the routes `service-ai` mounts:

| Method | Endpoint | Description |
|:-------|:---------|:------------|
| POST | `/ai/chat` | Chat completion (Vercel Data Stream or JSON) |
| POST | `/ai/chat/stream` | SSE streaming chat |
| POST | `/ai/complete` | Text completion |
| GET | `/ai/models` | Models this environment offers (ADR-0028) |
| GET | `/ai/status` | Active adapter provenance |
| GET | `/ai/effective-model` | Resolved model ids and their source |
| POST / GET | `/ai/conversations` | Create / list conversations |
| GET / PATCH / DELETE | `/ai/conversations/:id` | Read / update / delete |
| POST | `/ai/conversations/:id/messages` | Append a message |

<Callout type="warn">
**This table was inverted, and is corrected here (#3718).** The three routes
above are declared in `DEFAULT_AI_ROUTES` and called by
`client.ai.nlq/suggest/insights`, but **no plugin in any repo mounts them** —
every call 404s. Conversely, this callout used to state "there is no
`/ai/chat` route", which was wrong: `/ai/chat` is mounted.

What the AI plugin (`service-ai`) actually serves is a different set of
**12** routes:

| Method | Endpoint |
|:-------|:---------|
| POST | `/ai/chat`, `/ai/chat/stream`, `/ai/complete` |
| GET | `/ai/models`, `/ai/status`, `/ai/effective-model` |
| POST / GET | `/ai/conversations` |
| GET / PATCH / DELETE | `/ai/conversations/:id` |
| POST | `/ai/conversations/:id/messages` |

None of those has a client-SDK method — the SDK's `ai` namespace and the real
AI surface do not overlap at all. Reviewed dispositions for all 12 live in the
`cloud` repo, `packages/service-ai/src/ai-route-ledger.ts`.

The original point stands on its own terms: use `useChat()` (`@ai-sdk/react`)
directly against the streaming chat endpoint, because the client SDK exposes no
`chat` method.
**This table used to be inverted (#3718).** It listed `/ai/nlq`,
`/ai/suggest` and `/ai/insights` — none of which any plugin has ever mounted —
and its callout stated "there is no `/ai/chat` route", which was wrong.

The three phantom routes were declared in `DEFAULT_AI_ROUTES` and called by
`client.ai.nlq/suggest/insights`; every call 404ed. **v17 removed that SDK
namespace** rather than build endpoints for it, so nothing calls them now.

No route above has a client-SDK method yet — reach them directly, or with
`useChat()` (`@ai-sdk/react`) for chat, which speaks the Data Stream Protocol
`POST /ai/chat` serves. Reviewed dispositions for all 12 live in the `cloud`
repo, `packages/service-ai/src/ai-route-ledger.ts`.
</Callout>

### i18n (`/i18n`) — Plugin Required
Expand Down
11 changes: 6 additions & 5 deletions content/docs/kernel/services-checklist.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -252,11 +252,12 @@ Trigger engine, event triggers from ObjectQL hooks, flow executor, scheduled tri
`aiNlq`, `aiSuggest`, `aiInsights`

<Callout type="warn">
Declared only. All three are **optional** protocol methods (`aiNlq?` …) and no
service in any repo implements them, so the `/ai/{nlq,suggest,insights}` routes
they back are mounted by nothing and the matching `client.ai.*` calls 404
(#3718). The AI service that does exist (`service-ai`) serves a different set
— chat, complete, models, conversations — through none of these methods.
Declared only. All three are **optional** protocol methods (`aiNlq?` …) that no
service in any repo implements, so the `/ai/{nlq,suggest,insights}` routes they
back are mounted by nothing. The `client.ai` namespace that called them was
removed in v17 (#3718). The AI service that does exist (`service-ai`) serves a
different set — chat, complete, models, conversations — through none of these
methods, so this entry describes a contract with no implementer on either side.
</Callout>

### 11. i18n — 3 methods
Expand Down
48 changes: 16 additions & 32 deletions packages/client/src/client-url-conformance.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -151,28 +151,23 @@ function matches(verb: string, path: string): Pattern | undefined {
const CONTROL_PLANE = '/api/v1/cloud/';
const CONTROL_PLANE_NAMESPACE = 'projects.';

/**
* The AI surface — the SECOND cross-repo prefix, and exempt for the same
* reason as the control plane rather than a different one.
*
* `/api/v1/ai/*` routes are built by `service-ai`'s `buildAIRoutes()` at plugin
* start, and `service-ai` is a Cloud/EE package living in the `cloud` repo.
* This repo's dispatcher only proxies to whatever that table contains (or 404s
* "AI service is not configured" when it is absent), so no ledger here can
* enumerate them — the same boundary `projects.*` sits behind.
/*
* There is no AI exemption any more, and that is the end state — not an
* omission.
*
* It used to be handled as a `* /ai/**` WILDCARD match instead, which was
* strictly worse: a wildcard says the family is claimed, so all three `ai.*`
* methods counted as matched. #3718 enumerated the real table in `cloud` and
* found the SDK's three URLs are not in it — `/nlq`, `/suggest` and
* `/insights` are mounted by nothing, in any repo (#3718). The wildcard was
* not weak evidence, it was wrong evidence, which is exactly why the ratchet
* below treats `**` matches as something to drive to zero rather than tolerate.
* `/api/v1/ai/` was briefly exempted here the way the control plane still is:
* `service-ai` is a Cloud/EE package in the `cloud` repo, so no ledger here can
* enumerate its table. Before that it was a `* /ai/**` WILDCARD match, which
* was worse — a wildcard claims the family, so all three `ai.*` methods counted
* as matched when none of their URLs was in the real table at all (#3718).
*
* Bounded the same way as the control plane: only `ai.*` may use the prefix.
* v17 removed the `ai` namespace outright, so no SDK method targets the prefix
* and there is nothing left to exempt. If an `ai.*` method is ever added back,
* it will match no route here and fail the `unmatched` assertion below — which
* is correct: the real AI surface is ledgered in `cloud`
* (`packages/service-ai/src/ai-route-ledger.ts`), and a new method should be
* verified against it there rather than waved through by a prefix here.
*/
const AI_PLANE = '/api/v1/ai/';
const AI_NAMESPACE = 'ai.';

// ---------------------------------------------------------------------------
// 2. The recorder
Expand DownExpand Up@@ -352,7 +347,6 @@ describe('client URL conformance ↔ the union of all four route ledgers (#3642)
const silent: string[] = [];
const malformed: string[] = [];
const controlPlane: string[] = [];
const aiPlane: string[] = [];
const wildcardOnly: string[] = [];

for (const name of METHODS) {
Expand All@@ -374,7 +368,6 @@ describe('client URL conformance ↔ the union of all four route ledgers (#3642)
}
const path = new URL(call.url, BASE).pathname;
if (path.startsWith(CONTROL_PLANE)) { controlPlane.push(`${name} → ${call.verb} ${path}`); continue; }
if (path.startsWith(AI_PLANE)) { aiPlane.push(`${name} → ${call.verb} ${path}`); continue; }
const hit = matches(call.verb, path);
if (!hit) { unmatched.push(`${name} → ${call.verb} ${path}`); continue; }
if (hit.route.includes('**')) wildcardOnly.push(`${name} → ${call.verb} ${path} (via ${hit.route})`);
Expand DownExpand Up@@ -409,15 +402,6 @@ describe('client URL conformance ↔ the union of all four route ledgers (#3642)
).toEqual([]);
expect(controlPlane.length, 'the projects namespace should still be reaching the control plane').toBeGreaterThan(0);

// Same bounding for the AI prefix. `ai.*` is the ONLY namespace allowed to
// use it; anything else reaching /api/v1/ai/ is a method that has wandered
// into a surface no in-repo ledger can vouch for.
const aiTrespassers = aiPlane.filter((e) => !e.startsWith(AI_NAMESPACE));
expect(
aiTrespassers,
`non-ai methods targeting the AI plane, which service-ai owns in the cloud repo:\n${aiTrespassers.join('\n')}`,
).toEqual([]);
expect(aiPlane.length, 'the ai namespace should still be reaching the AI plane').toBeGreaterThan(0);

// HOW STRONG IS THIS GUARD, HONESTLY. A `**` row asserts only that a prefix
// family is CLAIMED, not that the specific URL resolves. That was this
Expand All@@ -428,8 +412,8 @@ describe('client URL conformance ↔ the union of all four route ledgers (#3642)
// THAT family (#3718, in `cloud`, where service-ai lives) showed the
// wildcard had not been weak evidence but WRONG evidence: none of the three
// URLs is in the real table, and nothing in any repo mounts them (#3718).
// They are now handled by the AI_PLANE exemption above and pinned as dead
// on the cloud side, so this bound is 0.
// v17 removed that namespace outright, so no SDK method targets `/ai/` and
// the bound is 0 with nothing exempted to get there.
//
// ZERO IS THE POINT: every remaining matched call rests on an exact route
// some ledger enumerated. Raising this bound reintroduces the one kind of
Expand Down
62 changes: 20 additions & 42 deletions packages/client/src/client.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -604,51 +604,29 @@ describe('Notifications namespace', () => {
});
});

describe('AI namespace', () => {
it('should execute natural language query', async () => {
const { client, fetchMock } = createMockClient({
success: true,
data: { query: { object: 'customer', where: {} }, confidence: 0.95 }
});
const result = await client.ai.nlq({ query: 'find all active customers' });
expect(result.confidence).toBe(0.95);
const [url, opts] = fetchMock.mock.calls[0];
expect(url).toContain('/api/v1/ai/nlq');
expect(opts.method).toBe('POST');
});

it('should not expose chat method (use Vercel AI SDK useChat directly)', () => {
describe('AI namespace (removed in v17 — #3718)', () => {
/**
* This block used to hold four passing tests for `ai.nlq`, `ai.suggest`
* and `ai.insights`. Every one of them mocked `fetch` and asserted the URL
* the client BUILT — never that anything answered it. All three endpoints
* were mounted by nothing, in any repo, for the whole life of those tests.
*
* That is the shape of test this audit family kept finding behind green
* suites (#3584, #3611, #3636, #3702), so the replacement asserts the one
* thing that is actually true and worth defending: the namespace is gone
* and must not come back without a route behind it.
*/
it('is gone — no method may return without an endpoint to answer it', () => {
const { client } = createMockClient({ success: true, data: {} });
// ai.chat was removed — consumers should use @ai-sdk/react useChat() directly
expect(client.ai).not.toHaveProperty('chat');
});

it('should get AI suggestions', async () => {
const { client, fetchMock } = createMockClient({
success: true,
data: { suggestions: ['Alice Corp', 'Alpha Inc'] }
});
const result = await client.ai.suggest({
object: 'customer',
field: 'name',
partial: 'Al'
});
expect(result.suggestions).toHaveLength(2);
expect((client as unknown as Record<string, unknown>).ai).toBeUndefined();
});

it('should get AI insights', async () => {
const { client, fetchMock } = createMockClient({
success: true,
data: { type: 'summary', insights: [] }
});
const result = await client.ai.insights({
object: 'order',
type: 'summary'
});
expect(result.type).toBe('summary');
const [url, opts] = fetchMock.mock.calls[0];
expect(url).toContain('/api/v1/ai/insights');
expect(opts.method).toBe('POST');
it('still directs chat at the Vercel AI SDK', () => {
// Unchanged guidance, and the reason no `chat` method is being added
// back with the real surface: `useChat()` (`@ai-sdk/react`) speaks the
// Data Stream Protocol against POST /api/v1/ai/chat directly.
const { client } = createMockClient({ success: true, data: {} });
expect((client as unknown as Record<string, unknown>).ai).toBeUndefined();
});
});

Expand Down
Loading
Loading