Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .changeset/decision-output-def-collapses-to-spec.md
Original file line number Diff line number Diff line change
@@ -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<keyof DecisionOutputDef, keyof SpecDecisionOutputDef>`
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.
22 changes: 13 additions & 9 deletions packages/app-shell/src/__tests__/spec-symbol-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Equal<IsAny<SpecDecisionOutputDef>, false>>;

// Every spec decision output is usable here.
type _SpecIsUsableHere = Assert<Extends<SpecDecisionOutputDef, DecisionOutputDef>>;

// `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<Exclude<keyof DecisionOutputDef, keyof SpecDecisionOutputDef>, '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<Exclude<keyof DecisionOutputDef, keyof SpecDecisionOutputDef>, never>
>;
type _IsExactlyTheSpecType = Assert<Equal<DecisionOutputDef, SpecDecisionOutputDef>>;

// 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.
Expand Down
32 changes: 18 additions & 14 deletions packages/app-shell/src/utils/decisionOutputParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.';
Expand Down
Loading