Skip to content
Merged
145 changes: 145 additions & 0 deletions .changeset/adr-0122-phase-2-bare-name-flip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
"@objectstack/spec": major
---

feat(spec)!: the bare type name is now the AUTHOR state — 1384 aliases flipped, 102 `XInput` synonyms retired (ADR-0122 phase 2, #6083)

A Zod schema denotes two types: `z.input` (what an author writes — defaulted keys
optional, pre-transform) and `z.infer` (what `.parse()` returns). Until protocol 17 the
bare name `X` meant the second one in 1384 places and the first one in 86, with nothing
recorded about which was which.

**[ADR-0122](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0122-schema-type-alias-naming-convention.md)
settles it: the bare name `X` is the AUTHOR state, `XParsed` is the PARSED state.**
Phase 1 (16.x, additive) gave every schema with two distinct shapes its `XParsed` name so
nothing would be stranded. **This release is phase 2: it flips the bare names.** It is the
breaking half, and it is the reason `@objectstack/spec` goes to 17.0.0.

```ts
// before (16.x) // after (17.0.0)
export type Connector = z.infer<…>; export type Connector = z.input<…>;
export type ConnectorParsed = z.infer<…>; export type ConnectorParsed = z.infer<…>;
export type ConnectorInput = z.input<…>; // ConnectorInput: RETIRED
```

## FROM → TO

There are exactly two migrations, and each has a mechanical test.

### 1. `XInput` → `X` (102 names removed)

The flip made `XInput` a character-for-character synonym of the bare name, and ADR-0122
D3 forbids a permanent synonym. Every retired name has the same fix: **drop the `Input`
suffix.**

```diff
- import type { ConnectorInput } from '@objectstack/spec/integration';
- const c: ConnectorInput = { name: 'acme', label: 'Acme', type: 'saas' };
+ import type { Connector } from '@objectstack/spec/integration';
+ const c: Connector = { name: 'acme', label: 'Acme', type: 'saas' };
```

Find them: `rg '\b\w+Input\b' --type ts` over your own code, then keep only the hits that
resolve to `@objectstack/spec`. Every one of them is a compile error on upgrade — there is
no silent failure in this direction, because the name is gone.

The 102 retired names, by module:

| module | retired |
|:---|:---|
| `api/auth` | `SessionUserInput`, `LoginRequestInput` |
| `api/dispatcher` | `DispatcherRouteInput`, `DispatcherConfigInput` |
| `api/endpoint` | `ApiEndpointInput` |
| `api/plugin-rest-api` | `RequestValidationConfigInput`, `ResponseEnvelopeConfigInput`, `ErrorHandlingConfigInput`, `OpenApiGenerationConfigInput`, `RestApiPluginConfigInput` |
| `api/protocol` | `NotificationPreferencesInput`, `NotificationInput` |
| `api/query-adapter` | `RestQueryAdapterInput`, `ODataQueryAdapterInput`, `QueryAdapterConfigInput` |
| `api/rest-server` | `RestApiConfigInput`, `CrudEndpointsConfigInput`, `MetadataEndpointsConfigInput`, `BatchEndpointsConfigInput`, `RouteGenerationConfigInput`, `RestServerConfigInput` |
| `api/versioning` | `VersioningConfigInput` |
| `automation` | `FlowFunctionDeclarationInput`, `ActionDescriptorInput`, `TimeRelativeTriggerInput`, `WebhookInput` |
| `data/analytics` | `CubeInput`, `AnalyticsQueryInput` |
| `data/datasource` | `DatasourceInput` |
| `data/field` | `FieldParseInput` (→ `Field`), `CurrencyConfigInput` |
| `data/mapping` | `MappingInput` |
| `data/object` | `ObjectFieldGroupInput`, `RowCrudActionOverrideInput`, `ServiceObjectInput`, `ObjectExtensionInput` |
| `data/seed`, `data/seed-loader` | `SeedInput`, `SeedLoaderConfigInput`, `SeedLoaderRequestInput` |
| `identity` | `EvalUserInput`, `PositionInput` |
| `integration/connector` | `ConnectorInput` |
| `kernel` | `ClusterCapabilityConfigInput`, `ExecutionContextInput`, `ObjectStackManifestInput`, `PackageArtifactInput`, `PluginVendorInput`, `PluginQualityMetricsInput`, `PluginStatisticsInput`, `PluginRegistryEntryInput`, `PluginSearchFiltersInput`, `PluginInstallConfigInput`, `ServiceRegistryConfigInput`, `StartupOptionsInput` |
| `security` | `ExplainRequestInput`, `AdminScopeInput`, `PermissionSetInput`, `SharingRuleInput` |
| `system` | `CacheTierInput`, `CacheConfigInput`, `DistributedCacheConfigInput`, `BackupConfigInput`, `FailoverConfigInput`, `DisasterRecoveryPlanInput`, `EmailTemplateDefinitionInput`, `KeyRotationPolicyInput`, `EncryptionConfigInput`, `FieldEncryptionInput`, `EnvironmentArtifactInput`, `RouteHandlerMetadataInput`, `MiddlewareConfigInput`, `ServerCapabilitiesInput`, `JobInput`, `FeatureInput`, `PlanInput`, `SecurityContextConfigInput`, `StackServerConfigInput`, `RowLevelIsolationStrategyInput`, `SchemaLevelIsolationStrategyInput`, `DatabaseLevelIsolationStrategyInput`, `TenantSecurityPolicyInput`, `TranslationBundleInput`, `TaskRetryPolicyInput`, `TaskInput`, `QueueConfigInput`, `BatchTaskInput`, `BatchProgressInput`, `WorkerConfigInput` |
| `ui` | `ActionInput`, `InlineActionInput`, `NavigationContributionInput`, `AppInput`, `DashboardInput`, `DatasetDimensionInput`, `DatasetMeasureInput`, `DatasetInput`, `PageInput`, `JoinedReportBlockInput`, `ReportInput`, `ReportChartInput`, `ReportSortInput`, `ThemeInput` |

**Nine `*Input` names are NOT retired** and need no change: `ExpressionInput`,
`CronExpressionInput`, `TemplateExpressionInput` and `PredicateInput` are the bare aliases
of their own `…InputSchema`, and `FormFieldInput`, `QueryInput`, `FieldInput`,
`ObjectStackDefinitionInput` and `NavigationItemInput` are composed types (recursive or
`Partial`-shaped) that no bare alias denotes.

### 2. `X` → `XParsed` **only where you hold a parse result**

If you annotate a value you *wrote*, do nothing — the bare name is now correct, and this
is the whole point of the change:

```ts
// This did not compile in 16.x unless you knew to write `ConnectorInput`.
// In 17.0.0 it is simply right, in every domain.
const c: Connector = { name: 'acme_erp', label: 'Acme ERP', type: 'saas' };
```

If you annotate a value that came *out of* `.parse()` (or out of a `defineX()` factory, or
off the wire after the engine parsed it) and you read a defaulted key from it, move that
annotation to `XParsed`:

```diff
- const parsed: Connector = ConnectorSchema.parse(raw);
+ const parsed: ConnectorParsed = ConnectorSchema.parse(raw);
if (parsed.enabled) { … } // `enabled` is `boolean` here, `boolean | undefined` on `Connector`
```

**The grep that finds these:** `rg 'Schema\.parse\(' -A2` and `rg ': *\w+ *= *await'` in
your own code, then check each annotation. **The reliable finder is the compiler**: every
site that reads a defaulted key off an author-state value is a `TS18048` /
`TS2532` ("possibly undefined") or a `TS2345`. Upgrade, run `tsc`, and fix what it names. In
this repo — 1127 files annotate a value with a spec type — that came to **40 files outside
`packages/spec`**, and every one of them was a compile error first, never a silent change.

**The one case tsc cannot name for you:** a *function's declared return type*. A parse
result is structurally assignable to the author state, so

```ts
function loadConnector(): Connector { return ConnectorSchema.parse(raw); } // still compiles!
```

keeps compiling while quietly promising callers less than it delivers. If you have
factories or loaders that return a parsed value, re-declare them as `XParsed` by hand.
`@objectstack/spec`'s own 24 `defineX` factories were migrated exactly this way —
`defineApp(...)` now returns `AppParsed`, `defineConnector(...)` returns `ConnectorParsed`,
and so on for every factory whose schema has two shapes.

## What did NOT change

- **No runtime behaviour.** Not one `.parse()` call, `.default()`, `.transform()` or schema
shape moved. This release changes which type name describes which value, nothing else.
- **`json-schema/` and `authorable-surface/` are byte-identical.** Those generators read
runtime `z.ZodType` exports, never type aliases.
- **Your metadata files.** `*.object.ts`, `*.view.ts`, connector and flow definitions
authored with `defineX(...)` are untouched. Bare-literal metadata files typed with
`XInput` need the suffix dropped and nothing else.

## Also in this release

- **`check:spec-parsed-alias` is inverted.** It used to require every bare `z.infer` alias
to be paired or pinned; the flip empties that population, so it now refuses a bare name
that reads `z.infer` (the flip, enforced), refuses an `XInput` synonym of a bare name
(the retirement, enforced), and keeps the paired-or-pinned and stale-pin arms on the
flipped form.
- **57 previously ungoverned aliases were audited.** Inverting the gate widened it to the
86 aliases that already read `z.input`, which phase 1 never examined. 22 gained an
`XParsed`; 35 were proved isomorphic and pinned, adding 35 to the pin registry (716 → 751
on the merged tree, after #5055's four retirements and #5775's one addition). This closes
#5507's remaining scope.
- **`@objectstack/spec` public surface: 106 export names removed, 24 added.** The removals
are the 102 `XInput` aliases (plus re-exports); the additions are the 22 new `XParsed`
names (plus re-exports). All type-only — no runtime code, no bundle-size change.

<!-- adr-0087: registered spec-type-alias-input-suffix-retired -->
4 changes: 2 additions & 2 deletions content/docs/getting-started/quick-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,10 @@ is gated at publish and, once it passes, serves real traffic. Full contract:

{/* os:check */}
```typescript
import type { ApiEndpointInput } from '@objectstack/spec/api';
import type { ApiEndpoint } from '@objectstack/spec/api';

// With `manifest: { namespace: 'acme', … }` on the same stack.
export const leadFeed: ApiEndpointInput = {
export const leadFeed: ApiEndpoint = {
name: 'acme_lead_feed',
path: '/api/v1/apps/acme/leads', // /api/v1/apps/<namespace>/<subpath>
method: 'GET',
Expand Down
14 changes: 7 additions & 7 deletions content/docs/kernel/contracts/data-engine.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ interface EngineQueryOptions {
search?: FullTextSearch; // Full-text search
expand?: Record<string, QueryAST>; // Recursive relation loading
distinct?: boolean; // SELECT DISTINCT
context?: ExecutionContextInput; // Identity, tenant, transaction — any subset
context?: ExecutionContext; // Identity, tenant, transaction — any subset
}
```

`ExecutionContextInput` is `ExecutionContext` with every field optional — supply
what you have, the engine reads what it needs. A system read passes
`ExecutionContext` is the author state — every field optional (the parse result,
with defaults applied, is `ExecutionContextParsed`). Supply what you have, the engine reads what it needs. A system read passes
`{ isSystem: true }`; an automation run with no resolvable identity passes only
its run id (`{ flowRunId }`), a context that deliberately carries no principal.

Expand Down Expand Up @@ -234,7 +234,7 @@ const tasks = await engine.insert('task', [
```typescript
interface DataEngineInsertOptions {
returning?: boolean; // Return inserted record(s)? Default: true
context?: ExecutionContextInput; // Identity, tenant, transaction — any subset
context?: ExecutionContext; // Identity, tenant, transaction — any subset
}
```

Expand All @@ -261,7 +261,7 @@ interface EngineUpdateOptions {
upsert?: boolean; // Insert if not found? Default: false
multi?: boolean; // Update multiple records? Default: false
returning?: boolean; // Return updated record(s)? Default: false
context?: ExecutionContextInput;
context?: ExecutionContext;
}
```

Expand Down Expand Up @@ -312,7 +312,7 @@ await engine.delete('task', {
interface EngineDeleteOptions {
where?: FilterCondition; // Filter to identify records (WHERE)
multi?: boolean; // Delete multiple records? Default: false
context?: ExecutionContextInput;
context?: ExecutionContext;
}
```

Expand Down Expand Up @@ -350,7 +350,7 @@ interface EngineAggregateOptions {
where?: FilterCondition; // Pre-aggregation filter (WHERE)
groupBy?: string[]; // GROUP BY fields
aggregations?: AggregationNode[]; // Aggregation definitions
context?: ExecutionContextInput;
context?: ExecutionContext;
}

interface AggregationNode {
Expand Down
Loading
Loading