Skip to content

Commit 5426cc7

Browse files
os-zhuangclaude
andauthored
refactor(app-shell): 把 DecisionOutputDef 收敛成 spec 的纯 re-export,并反转 parity pin (#3185)
* refactor(app-shell): collapse DecisionOutputDef to a plain spec re-export 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<keyof DecisionOutputDef, keyof SpecDecisionOutputDef>` 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5 * chore(changeset): DecisionOutputDef collapses to a plain spec re-export Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 022e4c3 commit 5426cc7

3 files changed

Lines changed: 69 additions & 23 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"@object-ui/app-shell": patch
3+
---
4+
5+
Collapse app-shell's `DecisionOutputDef` to a plain re-export of the spec's
6+
(objectstack#4562).
7+
8+
The local type was `interface DecisionOutputDef extends SpecDecisionOutputDef
9+
{ required?: boolean }` — a structural derivation carrying ONE documented
10+
divergence, because the server enforced `required` (`decide()` rejects a blank
11+
required output before any write) while `@objectstack/spec` did not model it.
12+
The spec adopted `required` in cd6b9f202 and pinned it at the schema level in
13+
objectstack#4561, and this repo now resolves a spec that has it
14+
(`@objectstack/spec@17.0.0-rc.1`, #3178). The addition is therefore redundant
15+
and the type becomes `export type DecisionOutputDef = SpecDecisionOutputDef`.
16+
17+
No behavior change and no API change: the symbol is internal to this package
18+
(it is not re-exported from `src/index.ts`), the resolved shape is identical
19+
key-for-key, and `decisionOutputParams()` still reads `d.required` — now off
20+
the spec's own field.
21+
22+
The module TSDoc still asserted "the spec does not model it yet", which was
23+
stale and actively misleading — an agent reading it would take the divergence
24+
as ground truth and build on it, which is the objectstack#4115 failure class
25+
this file's own tripwires exist to prevent. It now states the current truth.
26+
27+
The parity pin in `__tests__/spec-symbol-parity.test.ts` is inverted
28+
accordingly: `Exclude<keyof DecisionOutputDef, keyof SpecDecisionOutputDef>`
29+
is asserted `never` rather than `'required'`, plus an exact-identity
30+
assertion, so a future local addition to this symbol cannot slip in
31+
undocumented. The `type`-is-the-spec's-closed-enum pin is unchanged — that
32+
narrowing is still what stops a typo'd picker kind from silently degrading to
33+
a raw record-id text box (objectui#2955).
34+
35+
Note that this pin, like every other type-level assertion in that file, is not
36+
yet compiled by any gate — package tsconfigs exclude `**/*.test.ts`, so
37+
nothing type-checks it. It was verified by compiling the file explicitly. See
38+
objectui#3181.

packages/app-shell/src/__tests__/spec-symbol-parity.test.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
*
1313
* Twenty-eight app-shell symbols used to be declared under names the spec
1414
* already owns. Twenty were burned down by importing or deriving the spec's own
15-
* type (eighteen plain re-exports, plus `ScreenSpec` and `DecisionOutputDef`
16-
* derived structurally with one documented divergence each); eight were renamed
17-
* because they model something the spec's same-named export does not.
15+
* type (nineteen plain re-exports — `DecisionOutputDef` joined them once the
16+
* spec adopted `required`, objectstack#4562 — plus `ScreenSpec`, still derived
17+
* structurally with one documented divergence); eight were renamed because they
18+
* model something the spec's same-named export does not.
1819
*
1920
* One symbol is in both camps: the object designer's `FieldGroup` was renamed to
2021
* `ObjectFieldGroup` AND derived — the spec owns that exact shape, just under
@@ -232,19 +233,22 @@ describe('ScreenSpec derives from the spec, widening only `fields`', () => {
232233
});
233234
});
234235

235-
describe('DecisionOutputDef derives from the spec, adding only `required`', () => {
236+
describe('DecisionOutputDef is the spec type, with no local divergence left', () => {
236237
it('is pinned at compile time', () => {
237238
type _NotAny = Assert<Equal<IsAny<SpecDecisionOutputDef>, false>>;
238239

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

242-
// `required` is the ONLY local addition. When the spec adopts it, this
243-
// becomes `never`, the assertion fails, and the interface should collapse
244-
// to a plain re-export.
245-
type _OnlyRequiredAdded = Assert<
246-
Equal<Exclude<keyof DecisionOutputDef, keyof SpecDecisionOutputDef>, 'required'>
243+
// …and the reverse, because this is now a plain re-export rather than a
244+
// structural derivation. `required` used to be the ONE local addition; the
245+
// spec adopted it (cd6b9f202, pinned by objectstack#4561), so the interface
246+
// collapsed (objectstack#4562) and the exclusion set is empty. If a key ever
247+
// reappears here, this fails and the divergence has to be documented again.
248+
type _NoLocalAdditions = Assert<
249+
Equal<Exclude<keyof DecisionOutputDef, keyof SpecDecisionOutputDef>, never>
247250
>;
251+
type _IsExactlyTheSpecType = Assert<Equal<DecisionOutputDef, SpecDecisionOutputDef>>;
248252

249253
// Deriving NARROWED `type` from the bare `string` this file used to declare
250254
// to the spec's closed enum — that narrowing is the point, so pin it.

packages/app-shell/src/utils/decisionOutputParams.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,27 @@ import type { DecisionOutputDef as SpecDecisionOutputDef } from '@objectstack/sp
3939
/**
4040
* An approval node's declared decision output, as the server surfaces it.
4141
*
42-
* Derived from `@objectstack/spec/automation`'s `DecisionOutputDef` (structural
43-
* `extends`, objectstack#4115) with ONE local addition. Deriving also narrows
44-
* `type` from the bare `string` this file used to declare to the spec's closed
45-
* `'user' | 'department' | 'position' | 'team' | 'text'` enum — a typo'd kind
46-
* now fails to compile instead of silently degrading to a raw record-id text
47-
* box, which is the objectui#2955 failure this module exists to prevent.
42+
* A plain re-export of `@objectstack/spec/automation`'s `DecisionOutputDef` —
43+
* the spec models every key this module needs, so there is nothing left to
44+
* derive (objectstack#4562). The re-export is kept rather than pointing call
45+
* sites at the spec directly because this module is the ONE place decision
46+
* outputs become action params, and its consumers read the type from here.
4847
*
49-
* `required` is the documented divergence: the server enforces it (`decide()`
48+
* Two properties the re-export inherits are load-bearing here. `type` is the
49+
* spec's closed `'user' | 'department' | 'position' | 'team' | 'text'` enum,
50+
* so a typo'd kind fails to compile instead of silently degrading to a raw
51+
* record-id text box — the objectui#2955 failure this module exists to
52+
* prevent. `required` is now spec-modelled too (objectui#2955 shipped it
53+
* locally while the spec lagged; the spec adopted it in cd6b9f202, pinned at
54+
* the schema level by objectstack#4561): the server enforces it (`decide()`
5055
* rejects a blank required output before any write) and the dialog mirrors it
51-
* so the approver is stopped at the field rather than by a 400 — but the spec
52-
* does not model it yet. It is optional here so a backend predating the flag
53-
* still parses. When the spec adopts `required`, this interface collapses to a
54-
* plain re-export; `__tests__/spec-symbol-parity.test.ts` fails on that day.
56+
* so the approver is stopped at the field rather than by a 400. It stays
57+
* optional — a backend predating the flag still parses.
58+
*
59+
* `__tests__/spec-symbol-parity.test.ts` pins the exclusion set at empty, so
60+
* a future local addition to this symbol cannot slip in undocumented.
5561
*/
56-
export interface DecisionOutputDef extends SpecDecisionOutputDef {
57-
required?: boolean;
58-
}
62+
export type DecisionOutputDef = SpecDecisionOutputDef;
5963

6064
/** Param-name prefix the api handler folds back into the nested `outputs` body. */
6165
export const DECISION_OUTPUT_PARAM_PREFIX = 'outputs.';

0 commit comments

Comments
 (0)