Repository navigation
docs(spec): the AI slot answers 501, not 404, when no AI service is mounted - #18597
Merged
Merged
Conversation
…ounted `packages/spec/src/api/protocol.zod.ts`'s AI Operations note said the dispatcher "404s \"AI service is not configured\"" when nothing serves the slot. It answers 501 via the shared `capabilityUnavailable` exit, and the quoted body is no longer a local string — it comes from the shared `serviceUnavailableMessage`, so the 501 body and `discovery.services.ai` cannot drift. The replacement carries the same three arms the other three live sites gained in PR #17844: anonymous -> 401 first, `GET /ai/agents` -> 200 with an empty list as a console courtesy, every other `/ai/*` route -> 501. All three measured against `domains/ai-anonymous-deny-ordering.test.ts` (13/13 passing), not copied from the card. Prose only: no schema key moves, no accept set changes, no runtime edit. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-bill
marked this pull request as ready for review
September 17, 2026 07:51
os-bill
enabled auto-merge
September 17, 2026 07:52
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…inding nothing ever resolved (objectstack-ai#18619) Fixes objectstack-ai#16885 Clause-②: no Retires `ListViewSchema.navigation.view` under ADR-0049 enforce-or-remove, executing the maintainer ruling of 2026-09-13 (director decision batch objectstack-ai#126 item 4, comment `5651023571`, verbatim 「同意」, option **B**), with its item 5 superseded by comment `5657440531`: the changeset level is **`minor`**, not `major`. ## What the key did `navigation.view` was an unconstrained string whose describe promised *"the form view to use for details"*. No layer from spec to console ever resolved a view by that name. Its one read in the shipped console passed the value into the **second argument of `onNavigate`** — the slot that otherwise carries the navigation-MODE token — so an authored name did not select a view, it **substituted for the mode**. A consumer in the same bundle reads that argument against a closed two-value vocabulary (`edit` / `view`), so any other authored value matched neither branch. Declared, consumed, and wrong. ## Acceptance criteria — executable, with both controls **PROBE** — `navigation: { view: 'summary_view' }` is now refused, at the key the author wrote, with the prescription naming page assignment as the route: ``` PROBE navigation:{view:"summary_view"} -> success = false path = navigation.view message = `view.list.navigation.view` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — ... Delete the key; to choose what opens for a record, assign a `record` page to the object and let `isDefault` pick the one that opens — page assignment is the machinery that resolves a detail layout ... LIT navigation:{mode:"page"} -> success = true LIT all five surviving keys -> success = true ``` **LIT CONTROL** — the live siblings still parse. `{ mode: 'page' }` is accepted, and so are all five survivors together (`mode`, `preventNavigation`, `openNewTab`, `size`, `width`). A tombstone that broke its siblings would satisfy the refusal assertion while being a larger bug, and `navigation` is one closed shape, so that blast radius is the whole block. Pinned at all three doors — `ListViewSchema`, `ObjectListViewSchema`, and the flattened `PUT /api/v1/meta/view` overlay. **DARK CONTROL** — the reading used is the regenerated `packages/spec/authorable-surface/ui.json` `ui/NavigationConfig:*` row set. It returned: **6 rows before, 6 rows after**; exactly one line changed, `ui/NavigationConfig:view` -> `ui/NavigationConfig:view [RETIRED]`; the retired key never appears plain, and **no other `ui/NavigationConfig:*` row disappeared**. ```diff "ui/NavigationConfig:openNewTab", "ui/NavigationConfig:preventNavigation", "ui/NavigationConfig:size", - "ui/NavigationConfig:view", + "ui/NavigationConfig:view [RETIRED]", "ui/NavigationConfig:width", ``` ## Gates observed RED before GREEN A gate never observed failing for this change is not known to be a gate for it. | gate | red | green | | --- | --- | --- | | `check:generated` (`check:docs` leg) | `✗ 1 of 15 artifact(s) stale: content/docs/references/**` | `✓ All 15 generated artifacts are up to date.` after `--fix` regenerated exactly that one | | `check:generated` (`check:api-surface` leg) | `✗ ... dist/**/*.d.ts describe DIFFERENT sources than the ones on disk` (stale-dist trap, after the test file moved) | `✓` after a real rebuild | | `check-adr-0087-registration` | exit **1** — `declares a breaking change (BREAKING) but no adr-0087: disposition marker` — proven by committing the changeset with the marker line dropped | exit **0** — `1 declared-breaking changeset(s), each carrying an ADR-0087 disposition ... registered list-view-navigation-view-retired` | | the three refusal pins | **3 failed / 8 passed** under an ablation that restored the pre-retirement live string | **11 passed** on the real tree | The ablation mutated `packages/spec/src/ui/view.zod.ts` on disk (tombstone occurrences 1 -> 0, ablation marker 0 -> 1, blob hash moved), ran the pins, then restored under a `trap` — restored blob hash byte-identical to `HEAD` and `git diff HEAD` empty. Note the direction: only the three refusal pins went red; the eight sibling-acceptance assertions stayed green, which is the correct shape for reverting a tombstone. ## Gate results `pnpm --filter @objectstack/spec check:generated` — **all 15 green**, including the four named on the card: `check:authorable-surface`, `check:api-surface`, `check:docs`, `check:liveness`. Working tree clean afterwards (no regeneration drift). `pnpm --filter @objectstack/spec typecheck` — green. `pnpm --filter @objectstack/spec test` — **484 files, 13830 tests, all passed**. `pnpm lint` (repo-wide, `eslint . --no-inline-config`) — green, so no narrowing was taken and none is declared. All 14 source audits `check:generated` deliberately does not run — green. `check:skill-examples` first reported PREREQUISITE NOT MET (`packages/client-react/dist` unbuilt in a fresh worktree); after building that closure it reads `✅ 258 prose examples type-check across 3 surface(s)`. `node scripts/check-changeset-no-major.mjs` — green, no `major` bump. **`check-widening-tells` did not recur.** `node scripts/pm/check-widening-tells.mjs --declaration no --diff <this diff>` exits **0**: `10 changed file(s) — 4 judged against a declared surface (no widening tell), 6 NOT MEASURED`. The T1/T2 firing on `retiredKey()` lines recorded on objectstack-ai#17955 and objectstack-ai#17300 did **not** reproduce here, so there is nothing to report as a regression and the declaration is unchanged, as ruled. ## The ADR-0087 disposition is SEMANTIC, deliberately The ruling asked for a semantic migration entry, and the shape earns it: a mechanical D2 strip would delete the key without recording **which list view** lost it, and an author who wrote it wanted a named detail layout — a want page assignment serves and a stripped key does not record. So the entry is a D3 `SemanticMigration`, `list-view-navigation-view-retired`, and the tombstone prescription therefore carries **no** `os migrate meta` sentence: that sentence is owed only where a conversion covers the surface (`shared/retired-key.ts` module docblock; the class pin `retired-key-migrate-sentence.test.ts` deliberately judges nothing when the marker is absent). The precedent for registering a retired key with no D2 conversion is `data/AggregationNode:distinct` in this same table. Registered as `ui/NavigationConfig:view` in `RETIRED_KEYS_BY_MAJOR[18]`, which also starts its aging clock. Note for the reviewer: **no major-18 semantic entry reaches `spec-changes.json` or `docs/protocol-upgrade-guide.md` yet** — measured, not assumed: four sampled major-18 ids (`change-management-family-retired`, `training-family-retired`, `scim-provider-object-retired`, `epoch-instant-keys-renamed`) return 0 in both files, while major-17 ids return non-zero and the guide stops at "Protocol 17". This entry behaving the same way is the steady state for the open window, not a gap. ## Liveness ledger — nothing to update, and why `view/list/navigation` is one `live` row with **no classified children**, because `check-liveness` drills one level. That is objectstack-ai#17424's subject; it is cited here and **not fixed**, exactly as ruling item 4 directs. Since the walk never reaches `navigation`'s children, no ledger row exists for `view` to mark `dead`, and none is owed — `check:liveness` is green with the tombstone in place, with no UNCLASSIFIED row. `packages/spec/liveness/**` is untouched (held by objectstack-ai#18582). ## The objectui contract twin is in the SIBLING repo — reported, not touched Ruling item 3 asked where the contract twins live. **They are in `objectui`, not here.** The reading: - `objectui/packages/types/src/__tests__/view-navigation-config-spec-parity.test.ts` at the pinned `.objectui-sha` (`53ded82bf7a494f54e344e19099dbf00854b8694`) is the twin. It asserts `Equal<ViewNavigationConfig, NavigationConfig>` — `ViewNavigationConfig` **is** this spec type, re-exported by reference since objectui#4588. - That file **authors the retired key** as a legal value in four places, e.g. `const navigation: ViewNavigationConfig = { view: 'summary_view' };`. Once objectui resolves a spec carrying this tombstone, those lines become `tsc` errors under that package's `type-check`. - In **this** repo there is no twin pin covering the key: repo-wide, `ViewNavigationConfig` occurs only in `packages/console/CHANGELOG.md` (a release-owned file), and the tracked `sdui.manifest.json` declares `navigation` as a flat `object` input with no drill into `view`, so `check:react-declaration-parity` is structurally blind to it. Nothing here to update. **The Console Pin Gate is not affected and does not run on this PR.** Measured: that job is gated on the `console` paths filter (`.objectui-sha` plus four scripts), none of which this diff touches; and it runs `scripts/build-console.sh` — a **build**, which does not type-check the sibling's `tsconfig.test.json`. objectui's src at the pinned SHA contains **no value write** of `navigation.view` outside that test file (only docblocks), and reads of `.view` still compile against `never | undefined`. So this PR does not turn `main` red. What is still owed, in the other repository: the twin gains the tombstone pin before objectui picks up a spec carrying this retirement. Its dependency is `@objectstack/spec: ^17.0.0`, so a published 17.5.0 is in range. ## File surface Every path touched, including the pre-declared OPEN set. | path | why | | --- | --- | | `packages/spec/src/ui/view.zod.ts` | the `retiredKey()` tombstone + its prescription constant | | `packages/spec/src/migrations/entries/retired-keys/18.ui__NavigationConfig__view.ts` | new — `RETIRED_KEYS_BY_MAJOR[18]` registration (build-schemas gate (b)) | | `packages/spec/src/migrations/entries/semantic/18.list-view-navigation-view-retired.ts` | new — the ADR-0087 D3 semantic entry | | `packages/spec/src/ui/view.test.ts` | the pins: refusal at three doors, plus the lit controls | | `packages/spec/src/migrations/registry.ts` | OPEN set — generated by `gen:migration-registry`, never hand-edited | | `.changeset/16885-retire-navigation-view.md` | OPEN set — `minor`, `**BREAKING**` banner, FROM -> TO, ADR-0087 disposition | | `packages/spec/authorable-surface/ui.json` | OPEN set — regenerated by `build` | | `content/docs/references/ui/view.mdx` | OPEN set — regenerated by `gen:docs` | | `content/docs/references/api/protocol.mdx` | OPEN set — regenerated by `gen:docs` (the inline navigation summary drops `view?: string`) | | `content/docs/references/data/object.mdx` | OPEN set — same, one row | Held by other in-flight work and **not touched**: `packages/spec/scripts/build-schemas.ts` (objectstack-ai#17969), `packages/spec/liveness/**` and `packages/spec/scripts/liveness/check-liveness.mts` (objectstack-ai#18582), `packages/spec/src/api/protocol.zod.ts` (objectstack-ai#18597). Ruling item 2 was already discharged — PR objectstack-ai#17796 reads `closed`, not merged — so the retirement is written fresh. ## Acceptance notes Noted, not filed: nothing in the touched files. One finding **to file separately**, unrelated to this diff and not fixed here: `pnpm check:cross-package-test-inputs` reaches opposite verdicts on an identical source tree depending only on whether `packages/spec/dist/` happens to be built — exit **1** with it present (115 entries), exit **0** with the same directory moved aside, same commit, restored afterwards. The flagged radius is `packages/spec/dist/` descended from `packages/cli/test/init-created-files-summary.e2e.test.ts`, which this diff does not touch. No `pnpm build` step precedes `pnpm check:cross-package-test-inputs` in `lint.yml`'s `lint` job, so CI always runs it against an unbuilt tree and it passes vacuously — the declaration hole it exists to catch is structurally invisible on every PR. That is the "a verifier that silently degrades is worse than no verifier" shape from AGENTS.md Route & surface ownership rule 3. ## 维护者速读(草稿) **改了什么** —— `ListViewSchema.navigation.view` 退役:键仍在 shape 里,但变成 ADR-0049 墓碑, 写它是 `tsc` 报错、传值进来是带处方的 parse 拒收。处方点名替代路线:给对象发布 `record` 页面, 用 `isDefault` 决定打开哪一个。同批落地 ADR-0087 的 D3 语义迁移条目、changeset(`minor` + `**BREAKING**`)、以及三道门上的拒收 pin。 **为什么改** —— 这个键承诺"用哪个表单视图打开详情",而从 spec 到 console 没有任何一层按名字解析 视图。它唯一的读取把值塞进了 `onNavigate` 的**第二个参数** —— 那个槽位装的是导航**模式**令牌。 所以作者写的视图名不是在选视图,而是在**顶替模式**;而同一个 bundle 里有消费者拿这个参数去对 `edit`/`view` 两值词表,任何别的值两个分支都不匹配。仓库内零编写实例,唯一的外部作者已自行删除。 维护者 2026-09-13 裁决 B:零拉力即移除。 **风险与代价(含回滚)** —— 风险低。行为面没有回归可言:这个键从来没选中过任何东西,决定详情怎么 呈现的是 `mode` 和 `size`,两者一字未动,并且有 pin 钉着五个存活键仍然接受。代价是发布面:这是 **breaking**,升级时仍写着该键的元数据会在三道门(含 `PUT /api/v1/meta/view`)被拒。回滚就是 revert 本 PR —— 墓碑、注册条目、changeset 与生成产物都在同一个提交序列里,没有跨仓副作用。 一个待办留在**姊妹仓**:objectui 的契约孪生 pin 把 `{ view: 'summary_view' }` 写成合法值,需要在 objectui 取到带墓碑的 spec 之前补上墓碑 pin。本 PR 不触发 Console Pin Gate,不会让 `main` 变红。 **席位意见** —— **你要做的** —— --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #17847
Clause-②: no
packages/spec/src/api/protocol.zod.ts's AI Operations note said this repo's dispatcher "404sAI service is not configured" when nothing serves the AI slot. It has answered 501 since the sharedcapabilityUnavailableexit 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_01JbZnqu8bt6YqfJsr9vaFb3The 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/maine0d05538c0(unmoved for the life of this branch):Counter-side, read the same round:
packages/runtime/src/domains/ai.tsreachescapabilityUnavailable(deps, 'ai'), andpackages/runtime/src/domains/unavailable.tsreturnsdeps.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 configuredsurvives nowhere in the tree except that stale comment (grepoverpackages/,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 bypackages/runtime/src/domains/ai-anonymous-deny-ordering.test.ts, run on this tree:GET /ai/models→ 401, not the 501 remedy sentence ·GET /ai/agents→ 401, not the 200 empty-list courtesyANONYMOUS_DENY_STATUS = 401, pinned incore/src/security/anonymous-deny.test.ts)GET /ai/agents/ai/agentsas the declared envelope with the payload relocated underdata{ success: true, data: { agents: [] } }/ai/*/ai/models,/conversations,/usageand/chatwith the remedy sentence/discovery'sservices.aimessageserviceUnavailableMessageAI 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-changesetis not owed.packages/spec'sfiles[]carriessrc/**/*.zod.ts, andnpm pack --dry-run --jsonlistssrc/api/protocol.zod.tsamong the tarball's 2016 entries (positive control:README.mdpresent; negative control: zero*.test.ts). The sentence an upgrading author reads is a published byte, so it takes apatch. Notmajor, andClause-②: no—check:api-surfaceandcheck:authorable-surfaceboth 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:
Every exit code was landed to a file before being read — never through a pipe.
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-inputsexits 1 — the known signature (packages/clidescendspackages/spec/dist/), red on a tree wherepackages/spechas been built, green where it has not. Already filed as [finding] check:cross-package-test-inputs passes in CI and fails on a built tree — its verdict is a function of gitignored build state #18353 / [finding]check:cross-package-test-inputsanswers 1 or 0 depending on whetherpackages/spechas been BUILT — the author who follows AGENTS.md is the only one who sees the red, and CI never does #18440; ⛔ not filed again and not caused by this diff.check:dual-build-cjs-loadsandcheck:lean-entry-closureboth 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:docsincluded. The block is a bare//comment, not a.describe(), so it feeds nocontent/docs/references/**page:grep buildAIRoutes content/docs/references/is empty while the positive control (a.describe()string from this same file) does land incontent/docs/references/api/protocol.mdx. ⇒ nothing to regenerate, and nothing hand-edited under a generated tree.distrefusal (check:api-surface,check:browser-reachable-entries,check:dual-source-exports) — my src edit moved the build-input hash. All three exit 0 afterpnpm --filter @objectstack/spec build, which is the documented caveat working, not a finding.Tests
Dependency-closure build (step ①) is a no-op by construction:
packages/specdeclares no workspace dependencies, sopackages/spec^...is empty. The package itself was built, because thedist-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 fromeslint.config.mjsitself: every block matches**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}or a narrower glob, so the.changeset/*.mdhalf of this diff is outside the linted set entirely and one file is the whole of it. Invariance for untouched files: this repo runs oneeslint.config.mjsand it never enables type-aware linting — zero occurrences ofprojectService, and the only mention ofparserOptions.projectin 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-widepnpm lintstays 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.503 AI service routes not yet initializedexit 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