Skip to content

Commit d93400f

Browse files
os-billclaude
andauthored
docs(spec): the AI slot answers 501, not 404, when no AI service is mounted (#18597)
Fixes #17847 Clause-②: no `packages/spec/src/api/protocol.zod.ts`'s AI Operations note said this repo's dispatcher "404s `AI service is not configured`" when nothing serves the AI slot. It has answered **501** since the shared `capabilityUnavailable` exit landed, and the quoted body is no longer a local string either. This PR corrects that prose and nothing else — no runtime file is in the diff, and no status code, schema key or export moves. Attribution for this run, in prose so it survives any body rewrite: produced by Claude Code, session `session_01JbZnqu8bt6YqfJsr9vaFb3`, https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 ## The premise, re-measured — located by TEXT, not by line number The card warns its line numbers rotted three times in one day, so the sentence was re-located by its text against `origin/main` `e0d05538c0` (unmoved for the life of this branch): ``` $ grep -c '404s' packages/spec/src/api/protocol.zod.ts 1 $ grep -n '404s' packages/spec/src/api/protocol.zod.ts 2791:// `buildAIRoutes()` mounted, or 404s "AI service is not configured". So these ``` Counter-side, read the same round: `packages/runtime/src/domains/ai.ts` reaches `capabilityUnavailable(deps, 'ai')`, and `packages/runtime/src/domains/unavailable.ts` returns `deps.error(serviceUnavailableMessage(slot), 501)` under a docblock that states **501 Not Implemented** in words. The defect stands, and the premise is live. One extra reading the card did not carry: the literal string `AI service is not configured` survives **nowhere** in the tree except that stale comment (`grep` over `packages/`, `content/`, `examples/`, `scripts/` — one hit, the comment itself). So the sentence was stale in both halves: the status AND the body it quoted. ## Every arm was measured, not copied The card hands over its behaviour table explicitly unverified, and an unqualified "`/ai/*` answers 501" would manufacture a second inaccurate sentence. Each arm is pinned by `packages/runtime/src/domains/ai-anonymous-deny-ordering.test.ts`, run on this tree: ``` pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 --reporter=verbose \ src/domains/ai-anonymous-deny-ordering.test.ts Test Files 1 passed (1) Tests 13 passed (13) ``` | arm | the case that pins it | answer | |:--|:--|:--| | anonymous | `GET /ai/models` → 401, not the 501 remedy sentence · `GET /ai/agents` → 401, not the 200 empty-list courtesy | **401** first (`ANONYMOUS_DENY_STATUS = 401`, pinned in `core/src/security/anonymous-deny.test.ts`) | | `GET /ai/agents` | serves `/ai/agents` as the declared envelope with the payload relocated under `data` | **200**, `{ success: true, data: { agents: [] } }` | | every other `/ai/*` | 501s `/ai/models`, `/conversations`, `/usage` and `/chat` with the remedy sentence | **501** | | the body | keeps the 501 body string-identical to `/discovery`'s `services.ai` message | shared `serviceUnavailableMessage` | ⚠️ One condition deliberately **not** claimed in the new prose: a serveable service whose route table has not been published yet answers **503** `AI service routes not yet initialized`. That is a different premise from "no AI service is mounted", the sentence's own scope, and the three sibling sites do not state it either. Naming it here would widen the sentence back into the tour the card forbids. ## The replacement The same three-arm text the other three live sites carry after #16211 / PR #17844 (`packages/client/src/index.ts`, `packages/runtime/src/route-ledger.ts`, `packages/runtime/src/domains/ai.ts`), condensed to the block-comment voice this file already uses. 12 lines replace 2. ## Changeset: owed, and measured rather than assumed `skip-changeset` is **not** owed. `packages/spec`'s `files[]` carries `src/**/*.zod.ts`, and `npm pack --dry-run --json` lists `src/api/protocol.zod.ts` among the tarball's 2016 entries (positive control: `README.md` present; negative control: zero `*.test.ts`). The sentence an upgrading author reads is a published byte, so it takes a **`patch`**. Not `major`, and `Clause-②: no` — `check:api-surface` and `check:authorable-surface` both green prove no export and no authorable key moved. ## Gates Derived off the merge base by the repo's own deriver, never by a hand-written diff: ``` node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands # 77 commands node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RANFILE ✓ dispatch-gates --ran: 77 derived famil(ies) accounted for — 75 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3) ``` Every exit code was landed to a file before being read — never through a pipe. - **74 of 75 exit 0.** Plus the 6 artifact-roster families the deriver flags because their roster sits under one of my paths (`check-changeset-fixed`, `check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`) — read rather than assumed passed, all exit 0. - **`pnpm check:cross-package-test-inputs` exits 1** — the known signature (`packages/cli` descends `packages/spec/dist/`), red on a tree where `packages/spec` has been built, green where it has not. Already filed as #18353 / #18440; ⛔ not filed again and not caused by this diff. - **2 NOT-MEASURED, exit 3 = PREREQUISITE NOT MET:** `check:dual-build-cjs-loads` and `check:lean-entry-closure` both read built output across the whole workspace and refuse on an unbuilt package. Neither a pass nor a finding; they belong to CI's whole-tree build. - **`pnpm --filter @objectstack/spec check:generated` — all 15 generated artifacts up to date**, `check:docs` included. The block is a bare `//` comment, not a `.describe()`, so it feeds no `content/docs/references/**` page: `grep buildAIRoutes content/docs/references/` is empty while the positive control (a `.describe()` string from this same file) does land in `content/docs/references/api/protocol.mdx`. ⇒ nothing to regenerate, and nothing hand-edited under a generated tree. - **Three spec gates first answered exit 1 as a stale-`dist` refusal** (`check:api-surface`, `check:browser-reachable-entries`, `check:dual-source-exports`) — my src edit moved the build-input hash. All three exit 0 after `pnpm --filter @objectstack/spec build`, which is the documented caveat working, not a finding. ## Tests ``` pnpm --filter @objectstack/spec test Test Files 483 passed (483) · Tests 13781 passed (13781) pnpm --filter @objectstack/spec typecheck exit 0 (incl. check:test-typecheck, 144 pinned signatures held) ``` Dependency-closure build (step ①) is a no-op by construction: `packages/spec` declares no workspace dependencies, so `packages/spec^...` is empty. The package itself was built, because the `dist`-reading gates above require it. **Lint, as a declared narrowing rather than a repo sweep.** `pnpm exec eslint --no-inline-config --format json packages/spec/src/api/protocol.zod.ts` → **1 file, 0 errors, 0 warnings**, file count read from eslint's own JSON output. The checked population comes from `eslint.config.mjs` itself: every block matches `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` or a narrower glob, so the `.changeset/*.md` half of this diff is outside the linted set entirely and one file is the whole of it. Invariance for untouched files: this repo runs one `eslint.config.mjs` and it **never enables type-aware linting** — zero occurrences of `projectService`, and the only mention of `parserOptions.project` in the file is the comment stating it is absent — so no verdict on a file this diff did not touch can move because of this diff. The repo-wide `pnpm lint` stays CI's run. No ablation and no reverse-verification run is reported, and that is a statement rather than an omission: this diff adds no guard that could be ablated and changes no type that could be made to fail. The claim the prose makes is carried by an existing pin test, which is why it was run rather than read. ## Acceptance notes - `content/docs/api/plugin-endpoints.mdx` (hand-written) discusses the AI routes and does **not** repeat the false 404 claim — it simply says nothing about what an unmounted slot answers. An absence, not a wrong statement, so it is not one of the three fileable classes. noted, not filed; successor: none known — no queued PR touches that page's AI section. - The `503 AI service routes not yet initialized` exit noted above is correct as it stands and is deliberately left out of the corrected sentence. Recorded as a scope boundary, not a finding. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent a6a1de4 commit d93400f

2 files changed

Lines changed: 34 additions & 3 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): the AI Operations note said the slot 404s when no AI service is mounted — it has answered 501 since the shared `capabilityUnavailable` exit landed (#17847)
6+
7+
Clause-②: no — prose only. No schema key moves, no accept set widens or narrows, no export changes, and no runtime behaviour is touched; `packages/runtime` is not in this diff.
8+
9+
`src/api/protocol.zod.ts` ships inside this package (`files[]` carries `src/**/*.zod.ts`, and `npm pack --dry-run` lists `src/api/protocol.zod.ts` among its 2016 entries), so the sentence an author reads is a published byte. It said:
10+
11+
> this repo's dispatcher only proxies `/api/v1/ai/**` to whatever `buildAIRoutes()` mounted, or 404s "AI service is not configured"
12+
13+
Both halves were stale. `packages/runtime/src/domains/ai.ts` reaches the shared `capabilityUnavailable(deps, 'ai')` exit, which answers **501 Not Implemented**`/ai/*` IS mounted, so the request reaches a handler with nothing behind it, and 404 would claim the path does not exist. And the quoted body is no longer a local string: it comes from the shared `serviceUnavailableMessage`, the same sentence `discovery.services.ai` reports for the slot, so the 501 body and the discovery entry cannot drift into naming different remedies. The literal `AI service is not configured` survived nowhere in the tree except in that stale comment.
14+
15+
The replacement is the same three-arm text the other three live sites carry after #16211 / PR #17844 (`packages/client/src/index.ts`, `packages/runtime/src/route-ledger.ts`, `packages/runtime/src/domains/ai.ts`), because an unqualified "`/ai/*` answers 501" would manufacture a second inaccurate statement:
16+
17+
- an **anonymous** caller is refused **401** first (`ANONYMOUS_DENY_STATUS`), ahead of the slot being consulted — neither the 501 nor the courtesy below is owed to a caller who has not authenticated;
18+
- **`GET /ai/agents` answers 200** with an empty list (`{ agents: [] }` under the envelope's `data`) — a deliberate console courtesy, so polling does not log an error on every navigation;
19+
- every other `/ai/*` route answers **501** carrying the shared remedy sentence.
20+
21+
All three arms were measured rather than copied: `packages/runtime/src/domains/ai-anonymous-deny-ordering.test.ts` pins each of them and passes 13/13 on this tree.

packages/spec/src/api/protocol.zod.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2788,9 +2788,19 @@ export const MarkAllNotificationsReadResponseSchema = lazySchema(() => z.object(
27882788
//
27892789
// The AI service itself is a Cloud/EE package (`service-ai`, in the `cloud`
27902790
// repo) — this repo's dispatcher only proxies `/api/v1/ai/**` to whatever
2791-
// `buildAIRoutes()` mounted, or 404s "AI service is not configured". So these
2792-
// schemas deliberately describe **the wire**, not a protocol this repo serves:
2793-
// they are the one shape `client.ai.*` and cloud's route handlers both read.
2791+
// `buildAIRoutes()` mounted. With the service absent (the open-source
2792+
// default) the mount stays, so a request reaches a handler with nothing
2793+
// behind it and the answer is **501**, not 404 — 404 would mean the path does
2794+
// not exist, which for `/ai/*` is false. Two arms are narrower, and a caller
2795+
// branching on status needs both: an **anonymous** caller is refused **401**
2796+
// first (the 501 and the courtesy below are both capability disclosures), and
2797+
// `GET /ai/agents` answers **200** with an empty list (`{ agents: [] }` under
2798+
// the envelope's `data`) — a deliberate console courtesy. The 501 body is no
2799+
// local string: it comes from the shared `serviceUnavailableMessage`, the
2800+
// same sentence `discovery.services.ai` reports for the slot, so the two
2801+
// cannot drift into naming different remedies. So these schemas deliberately
2802+
// describe **the wire**, not a protocol this repo serves: they are the one
2803+
// shape `client.ai.*` and cloud's route handlers both read.
27942804
// The mounted table's reviewed dispositions live in `cloud`
27952805
// (`packages/service-ai/src/ai-route-ledger.ts`), which drives this SDK
27962806
// against the routes it really returns.

0 commit comments

Comments
 (0)