From 139b9df62675792b498d0cd9574bd6e19f8ea1c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 03:43:00 +0000 Subject: [PATCH 1/2] refactor(app-shell): collapse DecisionOutputDef to a plain spec re-export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BLOCKED — does not compile against the `@objectstack/spec` version this repo currently resolves (17.0.0-rc.0). Pushed to preserve the prepared change; see objectstack-ai/objectstack#4562 for the dependency decision it waits on. - `DecisionOutputDef` was `extends SpecDecisionOutputDef { required?: boolean }`. The spec adopted `required` (cd6b9f202, schema-pinned by objectstack#4561), so the derivation adds nothing and collapses to `export type DecisionOutputDef = SpecDecisionOutputDef`. - Rewrote the stale interface TSDoc, which still asserted "the spec does not model it yet". - Inverted the parity pin in `spec-symbol-parity.test.ts`: the exclusion set `Exclude` is now `never`, plus an exact-identity assertion. Verified against spec 17.0.0-rc.1 (which does model `required`): app-shell `tsc --noEmit` clean, the new assertions compile, and the old `'required'` assertion correctly fails (TS2344). Against the resolved 17.0.0-rc.0 the collapse fails with TS2339 `Property 'required' does not exist`. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5 --- .../src/__tests__/spec-symbol-parity.test.ts | 22 +++++++------ .../src/utils/decisionOutputParams.ts | 32 +++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/packages/app-shell/src/__tests__/spec-symbol-parity.test.ts b/packages/app-shell/src/__tests__/spec-symbol-parity.test.ts index a89683d4e..906dcd262 100644 --- a/packages/app-shell/src/__tests__/spec-symbol-parity.test.ts +++ b/packages/app-shell/src/__tests__/spec-symbol-parity.test.ts @@ -12,9 +12,10 @@ * * Twenty-eight app-shell symbols used to be declared under names the spec * already owns. Twenty were burned down by importing or deriving the spec's own - * type (eighteen plain re-exports, plus `ScreenSpec` and `DecisionOutputDef` - * derived structurally with one documented divergence each); eight were renamed - * because they model something the spec's same-named export does not. + * type (nineteen plain re-exports — `DecisionOutputDef` joined them once the + * spec adopted `required`, objectstack#4562 — plus `ScreenSpec`, still derived + * structurally with one documented divergence); eight were renamed because they + * model something the spec's same-named export does not. * * One symbol is in both camps: the object designer's `FieldGroup` was renamed to * `ObjectFieldGroup` AND derived — the spec owns that exact shape, just under @@ -232,19 +233,22 @@ describe('ScreenSpec derives from the spec, widening only `fields`', () => { }); }); -describe('DecisionOutputDef derives from the spec, adding only `required`', () => { +describe('DecisionOutputDef is the spec type, with no local divergence left', () => { it('is pinned at compile time', () => { type _NotAny = Assert, false>>; // Every spec decision output is usable here. type _SpecIsUsableHere = Assert>; - // `required` is the ONLY local addition. When the spec adopts it, this - // becomes `never`, the assertion fails, and the interface should collapse - // to a plain re-export. - type _OnlyRequiredAdded = Assert< - Equal, 'required'> + // …and the reverse, because this is now a plain re-export rather than a + // structural derivation. `required` used to be the ONE local addition; the + // spec adopted it (cd6b9f202, pinned by objectstack#4561), so the interface + // collapsed (objectstack#4562) and the exclusion set is empty. If a key ever + // reappears here, this fails and the divergence has to be documented again. + type _NoLocalAdditions = Assert< + Equal, never> >; + type _IsExactlyTheSpecType = Assert>; // Deriving NARROWED `type` from the bare `string` this file used to declare // to the spec's closed enum — that narrowing is the point, so pin it. diff --git a/packages/app-shell/src/utils/decisionOutputParams.ts b/packages/app-shell/src/utils/decisionOutputParams.ts index 6f7d1218c..e728dee34 100644 --- a/packages/app-shell/src/utils/decisionOutputParams.ts +++ b/packages/app-shell/src/utils/decisionOutputParams.ts @@ -39,23 +39,27 @@ import type { DecisionOutputDef as SpecDecisionOutputDef } from '@objectstack/sp /** * An approval node's declared decision output, as the server surfaces it. * - * Derived from `@objectstack/spec/automation`'s `DecisionOutputDef` (structural - * `extends`, objectstack#4115) with ONE local addition. Deriving also narrows - * `type` from the bare `string` this file used to declare to the spec's closed - * `'user' | 'department' | 'position' | 'team' | 'text'` enum — a typo'd kind - * now fails to compile instead of silently degrading to a raw record-id text - * box, which is the objectui#2955 failure this module exists to prevent. + * A plain re-export of `@objectstack/spec/automation`'s `DecisionOutputDef` — + * the spec models every key this module needs, so there is nothing left to + * derive (objectstack#4562). The re-export is kept rather than pointing call + * sites at the spec directly because this module is the ONE place decision + * outputs become action params, and its consumers read the type from here. * - * `required` is the documented divergence: the server enforces it (`decide()` + * Two properties the re-export inherits are load-bearing here. `type` is the + * spec's closed `'user' | 'department' | 'position' | 'team' | 'text'` enum, + * so a typo'd kind fails to compile instead of silently degrading to a raw + * record-id text box — the objectui#2955 failure this module exists to + * prevent. `required` is now spec-modelled too (objectui#2955 shipped it + * locally while the spec lagged; the spec adopted it in cd6b9f202, pinned at + * the schema level by objectstack#4561): the server enforces it (`decide()` * rejects a blank required output before any write) and the dialog mirrors it - * so the approver is stopped at the field rather than by a 400 — but the spec - * does not model it yet. It is optional here so a backend predating the flag - * still parses. When the spec adopts `required`, this interface collapses to a - * plain re-export; `__tests__/spec-symbol-parity.test.ts` fails on that day. + * so the approver is stopped at the field rather than by a 400. It stays + * optional — a backend predating the flag still parses. + * + * `__tests__/spec-symbol-parity.test.ts` pins the exclusion set at empty, so + * a future local addition to this symbol cannot slip in undocumented. */ -export interface DecisionOutputDef extends SpecDecisionOutputDef { - required?: boolean; -} +export type DecisionOutputDef = SpecDecisionOutputDef; /** Param-name prefix the api handler folds back into the nested `outputs` body. */ export const DECISION_OUTPUT_PARAM_PREFIX = 'outputs.'; From 3635ad88c7dd8f9369f42466a8ea9f079b9b6bf7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 08:19:40 +0000 Subject: [PATCH 2/2] chore(changeset): DecisionOutputDef collapses to a plain spec re-export Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5 --- .../decision-output-def-collapses-to-spec.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .changeset/decision-output-def-collapses-to-spec.md diff --git a/.changeset/decision-output-def-collapses-to-spec.md b/.changeset/decision-output-def-collapses-to-spec.md new file mode 100644 index 000000000..6d6de877d --- /dev/null +++ b/.changeset/decision-output-def-collapses-to-spec.md @@ -0,0 +1,38 @@ +--- +"@object-ui/app-shell": patch +--- + +Collapse app-shell's `DecisionOutputDef` to a plain re-export of the spec's +(objectstack#4562). + +The local type was `interface DecisionOutputDef extends SpecDecisionOutputDef +{ required?: boolean }` — a structural derivation carrying ONE documented +divergence, because the server enforced `required` (`decide()` rejects a blank +required output before any write) while `@objectstack/spec` did not model it. +The spec adopted `required` in cd6b9f202 and pinned it at the schema level in +objectstack#4561, and this repo now resolves a spec that has it +(`@objectstack/spec@17.0.0-rc.1`, #3178). The addition is therefore redundant +and the type becomes `export type DecisionOutputDef = SpecDecisionOutputDef`. + +No behavior change and no API change: the symbol is internal to this package +(it is not re-exported from `src/index.ts`), the resolved shape is identical +key-for-key, and `decisionOutputParams()` still reads `d.required` — now off +the spec's own field. + +The module TSDoc still asserted "the spec does not model it yet", which was +stale and actively misleading — an agent reading it would take the divergence +as ground truth and build on it, which is the objectstack#4115 failure class +this file's own tripwires exist to prevent. It now states the current truth. + +The parity pin in `__tests__/spec-symbol-parity.test.ts` is inverted +accordingly: `Exclude` +is asserted `never` rather than `'required'`, plus an exact-identity +assertion, so a future local addition to this symbol cannot slip in +undocumented. The `type`-is-the-spec's-closed-enum pin is unchanged — that +narrowing is still what stops a typo'd picker kind from silently degrading to +a raw record-id text box (objectui#2955). + +Note that this pin, like every other type-level assertion in that file, is not +yet compiled by any gate — package tsconfigs exclude `**/*.test.ts`, so +nothing type-checks it. It was verified by compiling the file explicitly. See +objectui#3181.