Skip to content

Commit a137bbc

Browse files
os-zhuangclaude
andauthored
feat(client)!: remove the ai namespace — three methods, none of which ever worked (#3718) (#3736)
* feat(client)!: remove the dead SDK surface — the `ai` namespace and `projects.listTemplates` (#3718, #3702) Four public methods, every one of them building a URL that NO server in any repo has ever mounted. All four 404ed from the release that shipped them: client.ai.nlq -> POST /api/v1/ai/nlq client.ai.suggest -> POST /api/v1/ai/suggest client.ai.insights -> POST /api/v1/ai/insights client.projects.listTemplates -> GET /api/v1/cloud/templates The three `ai.*` were declared in `DEFAULT_AI_ROUTES` — which has no runtime consumer, only the spec's own test reads it — and typed as optional protocol methods (`aiNlq?` …) nothing implements. `listTemplates` targeted a control-plane route that never existed; templates are a filtered `sys_package` view. All four were found by the #3563 audit's cross-repo guards, which match the URL each SDK method BUILDS against the routes each surface MOUNTS: `listTemplates` by the control-plane ledger (#3655), the three `ai.*` by the AI ledger (#3718), both living in `cloud` because that is the only repo where the mounted route set and the SDK are both in scope. 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. Lands in the v17 major `@objectstack/client` is already taking. That is the right window for removing public API — not a reason to defer it. Also removed: the `AI_PLANE` prefix exemption added to the capstone hours ago. 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 rather than deleted. They were the exact shape this audit family keeps finding behind green suites: mock `fetch`, assert the URL the client BUILT, never assert anything answered it — so they passed for 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}` stay 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 in the same pass: `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 routes service-ai really serves. FOLLOW-UP REQUIRED IN `cloud`, and it cannot be done before this merges: `UNMOUNTED_CLIENT_METHODS` and `UNMOUNTED_AI_CLIENT_METHODS` pin these exact four methods as existing-but-unmounted. When cloud's `.objectstack-sha` moves past this commit, those guards go red — correctly, by design — and the pins must be emptied in the same PR as the pin bump. client: 173 passed (13 files), tsc --noEmit clean, build 27/27. Co-Authored-By: Claude <noreply@anthropic.com> * chore: re-trigger CI GitHub Actions never fired for this branch — zero workflow runs across every workflow, only the Vercel check appeared. Close/reopen did not wake it either. An empty commit forces a `synchronize` event so the breaking change in this PR is actually verified rather than merged on an untested branch. Co-Authored-By: Claude <noreply@anthropic.com> * chore(changeset): rescope to the ai namespace only — main already removed listTemplates While this branch sat un-CI'd, #3702 landed on main independently and removed `projects.listTemplates` with near-identical reasoning. That was the merge conflict: both sides deleted the method and left a comment in its place. Took main's wording. So this PR no longer removes four methods, it removes three. The changeset said four; it now says what the diff does. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b070943 commit a137bbc

7 files changed

Lines changed: 158 additions & 168 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/client": major
3+
---
4+
5+
feat(client)!: remove the `ai` namespace — three methods, none of which ever worked (#3718)
6+
7+
`client.ai` held exactly three methods, and **no server in any repo has ever
8+
mounted the URLs they build**:
9+
10+
| Removed | Built | Why it 404ed |
11+
|---|---|---|
12+
| `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 |
13+
| `client.ai.suggest` | `POST /api/v1/ai/suggest` | same |
14+
| `client.ai.insights` | `POST /api/v1/ai/insights` | same |
15+
16+
Found by the AI route ledger (#3718, in `cloud`, where `service-ai` lives),
17+
which enumerates the table `buildAIRoutes()` returns and matches the SDK's URLs
18+
against it. The two sets are **disjoint**: the real AI surface is 12 routes —
19+
`chat`, `chat/stream`, `complete`, `models`, `status`, `effective-model` and six
20+
`conversations` routes — and the SDK expressed none of them.
21+
22+
**Removed, not deprecated.** A typed method that always throws is worse than no
23+
method: it costs a runtime round-trip to discover, where absence is a compile
24+
error. No working code can break, because there was no working behaviour. This
25+
lands in the v17 major `@objectstack/client` is already taking, which is the
26+
right window for a breaking removal rather than a reason to defer one.
27+
28+
Expressing the real surface is tracked on #3718 as **new** API, not a rename of
29+
what was removed. For chat, `useChat()` (`@ai-sdk/react`) already speaks the
30+
Data Stream Protocol `POST /api/v1/ai/chat` serves.
31+
32+
Also removed: the `AI_PLANE` exemption added to the capstone hours earlier
33+
(#3727). With no method targeting `/api/v1/ai/`, an exemption there is a hole
34+
with nothing to cover — the wildcard-only bound stays `0` and now reaches 0
35+
with nothing exempted to get there.
36+
37+
The four AI tests in `client.test.ts` are **replaced, not deleted**. They were
38+
the exact shape this audit keeps finding behind green suites: mock `fetch`,
39+
assert the URL the client *built*, never assert that anything answered it. They
40+
passed for years against three endpoints that did not exist. The replacement
41+
asserts the one thing worth defending — the namespace is gone and must not
42+
return without a route behind it.
43+
44+
`Ai{Nlq,Suggest,Insights}{Request,Response}` are still re-exported straight
45+
from `@objectstack/spec/api`, so anyone holding those types keeps them.
46+
Retiring the spec-side declarations is a separate change.
47+
48+
Docs corrected: `client-sdk.mdx` carried three copy-pasteable examples that
49+
404ed, and `plugin-endpoints.mdx` had the AI surface **inverted** — it tabled
50+
the three phantom routes and explicitly denied `/ai/chat`, which is mounted. It
51+
now lists the 12 real ones.

content/docs/api/client-sdk.mdx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -312,18 +312,21 @@ await client.notifications.markAllRead();
312312
// through sys_inbox_message and tracks read-state in sys_notification_receipt.
313313
// These helpers will be repointed during the objectui bell cut-over.
314314

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

328331
// i18n — Internationalization
329332
await client.i18n.getLocales();

content/docs/api/plugin-endpoints.mdx

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -95,37 +95,33 @@ The core dispatcher implements only the list / read / read-all routes above. Dev
9595

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

98-
| Method | Endpoint | Description | Status |
99-
|:-------|:---------|:------------|:-------|
100-
| POST | `/ai/nlq` | Natural language → query | **Not implemented** |
101-
| POST | `/ai/suggest` | Get value suggestions | **Not implemented** |
102-
| POST | `/ai/insights` | Get data insights | **Not implemented** |
98+
These are the routes `service-ai` mounts:
99+
100+
| Method | Endpoint | Description |
101+
|:-------|:---------|:------------|
102+
| POST | `/ai/chat` | Chat completion (Vercel Data Stream or JSON) |
103+
| POST | `/ai/chat/stream` | SSE streaming chat |
104+
| POST | `/ai/complete` | Text completion |
105+
| GET | `/ai/models` | Models this environment offers (ADR-0028) |
106+
| GET | `/ai/status` | Active adapter provenance |
107+
| GET | `/ai/effective-model` | Resolved model ids and their source |
108+
| POST / GET | `/ai/conversations` | Create / list conversations |
109+
| GET / PATCH / DELETE | `/ai/conversations/:id` | Read / update / delete |
110+
| POST | `/ai/conversations/:id/messages` | Append a message |
103111

104112
<Callout type="warn">
105-
**This table was inverted, and is corrected here (#3718).** The three routes
106-
above are declared in `DEFAULT_AI_ROUTES` and called by
107-
`client.ai.nlq/suggest/insights`, but **no plugin in any repo mounts them**
108-
every call 404s. Conversely, this callout used to state "there is no
109-
`/ai/chat` route", which was wrong: `/ai/chat` is mounted.
110-
111-
What the AI plugin (`service-ai`) actually serves is a different set of
112-
**12** routes:
113-
114-
| Method | Endpoint |
115-
|:-------|:---------|
116-
| POST | `/ai/chat`, `/ai/chat/stream`, `/ai/complete` |
117-
| GET | `/ai/models`, `/ai/status`, `/ai/effective-model` |
118-
| POST / GET | `/ai/conversations` |
119-
| GET / PATCH / DELETE | `/ai/conversations/:id` |
120-
| POST | `/ai/conversations/:id/messages` |
121-
122-
None of those has a client-SDK method — the SDK's `ai` namespace and the real
123-
AI surface do not overlap at all. Reviewed dispositions for all 12 live in the
124-
`cloud` repo, `packages/service-ai/src/ai-route-ledger.ts`.
125-
126-
The original point stands on its own terms: use `useChat()` (`@ai-sdk/react`)
127-
directly against the streaming chat endpoint, because the client SDK exposes no
128-
`chat` method.
113+
**This table used to be inverted (#3718).** It listed `/ai/nlq`,
114+
`/ai/suggest` and `/ai/insights` — none of which any plugin has ever mounted —
115+
and its callout stated "there is no `/ai/chat` route", which was wrong.
116+
117+
The three phantom routes were declared in `DEFAULT_AI_ROUTES` and called by
118+
`client.ai.nlq/suggest/insights`; every call 404ed. **v17 removed that SDK
119+
namespace** rather than build endpoints for it, so nothing calls them now.
120+
121+
No route above has a client-SDK method yet — reach them directly, or with
122+
`useChat()` (`@ai-sdk/react`) for chat, which speaks the Data Stream Protocol
123+
`POST /ai/chat` serves. Reviewed dispositions for all 12 live in the `cloud`
124+
repo, `packages/service-ai/src/ai-route-ledger.ts`.
129125
</Callout>
130126

131127
### i18n (`/i18n`) — Plugin Required

content/docs/kernel/services-checklist.mdx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -252,11 +252,12 @@ Trigger engine, event triggers from ObjectQL hooks, flow executor, scheduled tri
252252
`aiNlq`, `aiSuggest`, `aiInsights`
253253

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

262263
### 11. i18n — 3 methods

packages/client/src/client-url-conformance.test.ts

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -151,28 +151,23 @@ function matches(verb: string, path: string): Pattern | undefined {
151151
const CONTROL_PLANE = '/api/v1/cloud/';
152152
const CONTROL_PLANE_NAMESPACE = 'projects.';
153153

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

177172
// ---------------------------------------------------------------------------
178173
// 2. The recorder
@@ -352,7 +347,6 @@ describe('client URL conformance ↔ the union of all four route ledgers (#3642)
352347
const silent: string[] = [];
353348
const malformed: string[] = [];
354349
const controlPlane: string[] = [];
355-
const aiPlane: string[] = [];
356350
const wildcardOnly: string[] = [];
357351

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

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

422406
// HOW STRONG IS THIS GUARD, HONESTLY. A `**` row asserts only that a prefix
423407
// family is CLAIMED, not that the specific URL resolves. That was this
@@ -428,8 +412,8 @@ describe('client URL conformance ↔ the union of all four route ledgers (#3642)
428412
// THAT family (#3718, in `cloud`, where service-ai lives) showed the
429413
// wildcard had not been weak evidence but WRONG evidence: none of the three
430414
// URLs is in the real table, and nothing in any repo mounts them (#3718).
431-
// They are now handled by the AI_PLANE exemption above and pinned as dead
432-
// on the cloud side, so this bound is 0.
415+
// v17 removed that namespace outright, so no SDK method targets `/ai/` and
416+
// the bound is 0 with nothing exempted to get there.
433417
//
434418
// ZERO IS THE POINT: every remaining matched call rests on an exact route
435419
// some ledger enumerated. Raising this bound reintroduces the one kind of

packages/client/src/client.test.ts

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -604,51 +604,29 @@ describe('Notifications namespace', () => {
604604
});
605605
});
606606

607-
describe('AI namespace', () => {
608-
it('should execute natural language query', async () => {
609-
const { client, fetchMock } = createMockClient({
610-
success: true,
611-
data: { query: { object: 'customer', where: {} }, confidence: 0.95 }
612-
});
613-
const result = await client.ai.nlq({ query: 'find all active customers' });
614-
expect(result.confidence).toBe(0.95);
615-
const [url, opts] = fetchMock.mock.calls[0];
616-
expect(url).toContain('/api/v1/ai/nlq');
617-
expect(opts.method).toBe('POST');
618-
});
619-
620-
it('should not expose chat method (use Vercel AI SDK useChat directly)', () => {
607+
describe('AI namespace (removed in v17 — #3718)', () => {
608+
/**
609+
* This block used to hold four passing tests for `ai.nlq`, `ai.suggest`
610+
* and `ai.insights`. Every one of them mocked `fetch` and asserted the URL
611+
* the client BUILT — never that anything answered it. All three endpoints
612+
* were mounted by nothing, in any repo, for the whole life of those tests.
613+
*
614+
* That is the shape of test this audit family kept finding behind green
615+
* suites (#3584, #3611, #3636, #3702), so the replacement asserts the one
616+
* thing that is actually true and worth defending: the namespace is gone
617+
* and must not come back without a route behind it.
618+
*/
619+
it('is gone — no method may return without an endpoint to answer it', () => {
621620
const { client } = createMockClient({ success: true, data: {} });
622-
// ai.chat was removed — consumers should use @ai-sdk/react useChat() directly
623-
expect(client.ai).not.toHaveProperty('chat');
624-
});
625-
626-
it('should get AI suggestions', async () => {
627-
const { client, fetchMock } = createMockClient({
628-
success: true,
629-
data: { suggestions: ['Alice Corp', 'Alpha Inc'] }
630-
});
631-
const result = await client.ai.suggest({
632-
object: 'customer',
633-
field: 'name',
634-
partial: 'Al'
635-
});
636-
expect(result.suggestions).toHaveLength(2);
621+
expect((client as unknown as Record<string, unknown>).ai).toBeUndefined();
637622
});
638623

639-
it('should get AI insights', async () => {
640-
const { client, fetchMock } = createMockClient({
641-
success: true,
642-
data: { type: 'summary', insights: [] }
643-
});
644-
const result = await client.ai.insights({
645-
object: 'order',
646-
type: 'summary'
647-
});
648-
expect(result.type).toBe('summary');
649-
const [url, opts] = fetchMock.mock.calls[0];
650-
expect(url).toContain('/api/v1/ai/insights');
651-
expect(opts.method).toBe('POST');
624+
it('still directs chat at the Vercel AI SDK', () => {
625+
// Unchanged guidance, and the reason no `chat` method is being added
626+
// back with the real surface: `useChat()` (`@ai-sdk/react`) speaks the
627+
// Data Stream Protocol against POST /api/v1/ai/chat directly.
628+
const { client } = createMockClient({ success: true, data: {} });
629+
expect((client as unknown as Record<string, unknown>).ai).toBeUndefined();
652630
});
653631
});
654632

0 commit comments

Comments
 (0)