From 7357015143e0f88d9e703811caf6295167f43b85 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 06:05:39 +0000 Subject: [PATCH] docs(runtime,rest): name both causes of `next: null` in the state-introspection comments Both dispatchers compute `const next = from === undefined ? null : legalNextStates(schema, field, from);` so `next: null` has two causes -- no `state_machine` rule governs the field, and the caller omitted `?from=`. The comment immediately above each line named only the first. The `rest-server.ts` one was the sharper miss: it asserted a three-valued answer and justified the tri-state on the grounds that a UI must be able to tell the cases apart, directly above the line that folds a fourth input condition onto the same `null`. Both comments now match the semantics already asserted in `docs/qa/platform-checklist/areas/api-backend.json` and the prose in `content/docs/protocol/objectql/state-machine.mdx`. Comment text only -- every changed line is a `//` comment; no expression, return shape or observable behaviour moves. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR --- packages/rest/src/rest-server.ts | 14 ++++++++++---- packages/runtime/src/domains/meta.ts | 9 +++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 96503ffc4b..95c26ffd28 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -6296,10 +6296,16 @@ export class RestServer { }); return; } - // `next: null` = no FSM governs the field; `next: []` = - // a declared dead end. Same three-valued answer the - // dispatcher gives, because a UI asking "where can this - // record go" must be able to tell those apart. + // Three answer values — `next: null`, `next: []` (a + // declared dead end), and the legal-next list — the same + // answer the dispatcher gives, because a UI asking "where + // can this record go" must tell those apart. But `null` + // is overloaded across TWO input conditions: no FSM + // governs the field, or the caller omitted `?from=` (no + // `from` => no transition table to answer with), which + // the line below folds onto the same `null` without + // consulting the rule. A UI therefore cannot read `null` + // as "no state machine" unless it passed a `from`. const next = from === undefined ? null : legalNextStates(schema, field, from); res.json({ object: name, field, from: from ?? null, next }); } catch (error: any) { diff --git a/packages/runtime/src/domains/meta.ts b/packages/runtime/src/domains/meta.ts index 6de7ad462f..a8bc150b8a 100644 --- a/packages/runtime/src/domains/meta.ts +++ b/packages/runtime/src/domains/meta.ts @@ -232,8 +232,13 @@ export async function handleMetadataRequest(deps: DomainHandlerDeps, path: strin // ADR-0020 D3.3 introspection: the legal next states declared by the // object's `state_machine` validation rule for `:field`. Lets UIs / // AI authors ask "from here, where can this record go?" instead of - // hard-coding the transition table. Returns `next: null` when no FSM - // governs the field, `next: []` for a declared dead-end state. + // hard-coding the transition table. `next: []` is a declared + // dead-end state. `next: null` has TWO causes: no FSM governs the + // field, or the caller omitted `?from=` (no `from` => no transition + // table to answer with) — the handler short-circuits on that before + // it ever consults the rule. So a `null` answered to a call + // that passed no `from` is not evidence the field has no state + // machine; re-ask with `?from=`. if (parts.length === 4 && (parts[0] === 'objects' || parts[0] === 'object') && parts[2] === 'state' && (!method || method === 'GET')) { const name = parts[1]; const field = parts[3];