From b50dab7b97583821015857aeb8d5d8d4c05872a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 01:04:48 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(client)!:=20remove=20the=20dead=20SDK?= =?UTF-8?q?=20surface=20=E2=80=94=20the=20`ai`=20namespace=20and=20`projec?= =?UTF-8?q?ts.listTemplates`=20(#3718,=20#3702)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .changeset/remove-dead-sdk-surface.md | 47 ++++++++++ content/docs/api/client-sdk.mdx | 27 +++--- content/docs/api/plugin-endpoints.mdx | 54 ++++++------ content/docs/kernel/services-checklist.mdx | 11 +-- .../client/src/client-url-conformance.test.ts | 48 ++++------- packages/client/src/client.test.ts | 62 +++++-------- packages/client/src/index.ts | 86 +++++++------------ 7 files changed, 159 insertions(+), 176 deletions(-) create mode 100644 .changeset/remove-dead-sdk-surface.md diff --git a/.changeset/remove-dead-sdk-surface.md b/.changeset/remove-dead-sdk-surface.md new file mode 100644 index 0000000000..6390ea54e8 --- /dev/null +++ b/.changeset/remove-dead-sdk-surface.md @@ -0,0 +1,47 @@ +--- +"@objectstack/client": major +--- + +feat(client)!: remove the dead SDK surface — the `ai` namespace and `projects.listTemplates` (#3718, #3702) + +Four public methods are gone. Every one of them built a URL that **no server in +any repo has ever mounted**, so every call 404ed from the release that shipped +them: + +| Removed | Built | Why it never worked | +|---|---|---| +| `client.ai.nlq` | `POST /api/v1/ai/nlq` | declared in `DEFAULT_AI_ROUTES`, which has no runtime consumer; `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 | +| `client.projects.listTemplates` | `GET /api/v1/cloud/templates` | never mounted by the control plane; templates are a filtered `sys_package` view, not a route | + +All four were found by the #3563 route audit's cross-repo guards, which match +the URL each SDK method *builds* against the routes each surface *mounts* — +`projects.listTemplates` by the control-plane ledger (#3655) and the three +`ai.*` by the AI ledger (#3718), both in `cloud`. + +**Removed rather than 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. Nothing can depend on the old behaviour, because there was none +— no working code breaks. + +This lands in the v17 major that `@objectstack/client` is already taking, which +is the right window for removing public API rather than a reason to defer it. + +**What the AI surface actually is.** `service-ai` (Cloud/EE) serves 12 routes — +`chat`, `chat/stream`, `complete`, `models`, `status`, `effective-model`, and +six `conversations` routes. The SDK expressed none of them, so its `ai` +namespace and the real AI surface were disjoint sets. Expressing the real one +is tracked on #3718 as **new** API, not as a rename of what was removed. For +chat, `useChat()` (`@ai-sdk/react`) already speaks the Data Stream Protocol +`POST /api/v1/ai/chat` serves. + +`Ai{Nlq,Suggest,Insights}{Request,Response}` are still re-exported from +`@objectstack/spec/api`, so anyone holding those types keeps them while the +spec still declares 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). diff --git a/content/docs/api/client-sdk.mdx b/content/docs/api/client-sdk.mdx index 97ebc79037..615b6eac8d 100644 --- a/content/docs/api/client-sdk.mdx +++ b/content/docs/api/client-sdk.mdx @@ -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(); diff --git a/content/docs/api/plugin-endpoints.mdx b/content/docs/api/plugin-endpoints.mdx index e973f84595..a57315e72f 100644 --- a/content/docs/api/plugin-endpoints.mdx +++ b/content/docs/api/plugin-endpoints.mdx @@ -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 | -**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`. ### i18n (`/i18n`) — Plugin Required diff --git a/content/docs/kernel/services-checklist.mdx b/content/docs/kernel/services-checklist.mdx index 84d27254ac..1916f5468b 100644 --- a/content/docs/kernel/services-checklist.mdx +++ b/content/docs/kernel/services-checklist.mdx @@ -252,11 +252,12 @@ Trigger engine, event triggers from ObjectQL hooks, flow executor, scheduled tri `aiNlq`, `aiSuggest`, `aiInsights` -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. ### 11. i18n — 3 methods diff --git a/packages/client/src/client-url-conformance.test.ts b/packages/client/src/client-url-conformance.test.ts index 1fec2244e2..ed90cd2646 100644 --- a/packages/client/src/client-url-conformance.test.ts +++ b/packages/client/src/client-url-conformance.test.ts @@ -148,28 +148,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 @@ -349,7 +344,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) { @@ -371,7 +365,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})`); @@ -406,15 +399,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 @@ -425,8 +409,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 diff --git a/packages/client/src/client.test.ts b/packages/client/src/client.test.ts index e5de385cbb..1d2a0c6d4f 100644 --- a/packages/client/src/client.test.ts +++ b/packages/client/src/client.test.ts @@ -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).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).ai).toBeUndefined(); }); }); diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index f25c0e17fd..6922cee393 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -29,12 +29,10 @@ import { ListNotificationsResponse, MarkNotificationsReadResponse, MarkAllNotificationsReadResponse, - AiNlqRequest, - AiNlqResponse, - AiSuggestRequest, - AiSuggestResponse, - AiInsightsRequest, - AiInsightsResponse, + // Ai{Nlq,Suggest,Insights}{Request,Response} are no longer imported: the + // `ai` namespace that used them is gone in v17 (#3718). They are still + // RE-EXPORTED below, straight from `@objectstack/spec/api`, so anyone + // holding those types keeps them while the spec still declares them. GetLocalesResponse, GetTranslationsResponse, GetFieldLabelsResponse, @@ -1274,14 +1272,11 @@ export class ObjectStackClient { return this.unwrapResponse<{ drivers: Array<{ name: string; driverId: string }>; total: number }>(res); }, - /** - * List available project templates. Templates are seeded into the project - * database once at provisioning time when `template_id` is supplied. - */ - listTemplates: async () => { - const res = await this.fetch(`${this.baseUrl}/api/v1/cloud/templates`); - return this.unwrapResponse<{ templates: Array<{ id: string; label: string; description: string; category?: string }>; total: number }>(res); - }, + // `listTemplates` removed in v17 (#3702). It called + // `GET /api/v1/cloud/templates`, which no registrar in this repo or the + // control plane has ever mounted — the string appeared exactly once per + // repo, in the call itself. Templates exist as a filtered `sys_package` + // view (`is_starter = true`), never as an HTTP route, so every call 404ed. /** * Per-project package installation management (Power Apps "solution" model). @@ -3455,48 +3450,27 @@ export class ObjectStackClient { } }; - /** - * AI Services - */ - ai = { - /** - * Natural language query — converts natural language to structured query - */ - nlq: async (request: AiNlqRequest): Promise => { - const route = this.getRoute('ai'); - const res = await this.fetch(`${this.baseUrl}${route}/nlq`, { - method: 'POST', - body: JSON.stringify(request) - }); - return this.unwrapResponse(res); - }, - - // AI chat method removed — use Vercel AI SDK `useChat()` / `@ai-sdk/react` directly. - - /** - * AI-powered field value suggestions - */ - suggest: async (request: AiSuggestRequest): Promise => { - const route = this.getRoute('ai'); - const res = await this.fetch(`${this.baseUrl}${route}/suggest`, { - method: 'POST', - body: JSON.stringify(request) - }); - return this.unwrapResponse(res); - }, - - /** - * AI-powered data insights - */ - insights: async (request: AiInsightsRequest): Promise => { - const route = this.getRoute('ai'); - const res = await this.fetch(`${this.baseUrl}${route}/insights`, { - method: 'POST', - body: JSON.stringify(request) - }); - return this.unwrapResponse(res); - } - }; + // The `ai` namespace is GONE in v17 (#3718). + // + // It held exactly three methods — `nlq`, `suggest`, `insights` — building + // `/api/v1/ai/{nlq,suggest,insights}`. Nothing in any repo ever mounted those + // paths: they were declared in `DEFAULT_AI_ROUTES` (which has no runtime + // consumer) and typed as optional protocol methods (`aiNlq?` …) nothing + // implements. Every call 404ed, from the first release that shipped them. + // + // What DOES exist is a different surface entirely, served by `service-ai` + // (Cloud/EE): `POST /ai/chat`, `/ai/chat/stream`, `/ai/complete`, + // `GET /ai/models`, and six `/ai/conversations` routes. The SDK expressed + // none of them, so its AI namespace and the real AI surface were disjoint + // sets. Ledgered in `cloud`: `packages/service-ai/src/ai-route-ledger.ts`. + // + // Deliberately removed rather than left 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. Expressing the real surface is + // tracked separately on #3718; it is a new API, not a rename of this one. + // + // For chat specifically the answer is unchanged: use the Vercel AI SDK + // (`useChat()` from `@ai-sdk/react`) directly against the chat endpoint. /** * Internationalization Services From 5f1a4511aa50551eea7f585332950fd3af549b7c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 01:24:38 +0000 Subject: [PATCH 2/3] chore: re-trigger CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 From b316020f128194103c4deefc5a64f94dbcb5e908 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 02:08:37 +0000 Subject: [PATCH 3/3] =?UTF-8?q?chore(changeset):=20rescope=20to=20the=20ai?= =?UTF-8?q?=20namespace=20only=20=E2=80=94=20main=20already=20removed=20li?= =?UTF-8?q?stTemplates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .changeset/remove-dead-sdk-surface.md | 80 ++++++++++++++------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/.changeset/remove-dead-sdk-surface.md b/.changeset/remove-dead-sdk-surface.md index 6390ea54e8..d8f749fd27 100644 --- a/.changeset/remove-dead-sdk-surface.md +++ b/.changeset/remove-dead-sdk-surface.md @@ -2,46 +2,50 @@ "@objectstack/client": major --- -feat(client)!: remove the dead SDK surface — the `ai` namespace and `projects.listTemplates` (#3718, #3702) +feat(client)!: remove the `ai` namespace — three methods, none of which ever worked (#3718) -Four public methods are gone. Every one of them built a URL that **no server in -any repo has ever mounted**, so every call 404ed from the release that shipped -them: +`client.ai` held exactly three methods, and **no server in any repo has ever +mounted the URLs they build**: -| Removed | Built | Why it never worked | +| Removed | Built | Why it 404ed | |---|---|---| -| `client.ai.nlq` | `POST /api/v1/ai/nlq` | declared in `DEFAULT_AI_ROUTES`, which has no runtime consumer; `aiNlq?` is an optional protocol method nothing implements | +| `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 | -| `client.projects.listTemplates` | `GET /api/v1/cloud/templates` | never mounted by the control plane; templates are a filtered `sys_package` view, not a route | - -All four were found by the #3563 route audit's cross-repo guards, which match -the URL each SDK method *builds* against the routes each surface *mounts* — -`projects.listTemplates` by the control-plane ledger (#3655) and the three -`ai.*` by the AI ledger (#3718), both in `cloud`. - -**Removed rather than 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. Nothing can depend on the old behaviour, because there was none -— no working code breaks. - -This lands in the v17 major that `@objectstack/client` is already taking, which -is the right window for removing public API rather than a reason to defer it. - -**What the AI surface actually is.** `service-ai` (Cloud/EE) serves 12 routes — -`chat`, `chat/stream`, `complete`, `models`, `status`, `effective-model`, and -six `conversations` routes. The SDK expressed none of them, so its `ai` -namespace and the real AI surface were disjoint sets. Expressing the real one -is tracked on #3718 as **new** API, not as a rename of what was removed. For -chat, `useChat()` (`@ai-sdk/react`) already speaks the Data Stream Protocol -`POST /api/v1/ai/chat` serves. - -`Ai{Nlq,Suggest,Insights}{Request,Response}` are still re-exported from -`@objectstack/spec/api`, so anyone holding those types keeps them while the -spec still declares 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). + +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.