diff --git a/.changeset/docs-index-category-keyed.md b/.changeset/docs-index-category-keyed.md new file mode 100644 index 0000000000..42a055c1c4 --- /dev/null +++ b/.changeset/docs-index-category-keyed.md @@ -0,0 +1,57 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): the reference docs index is keyed by `/`, so a schema is documented on the page of the file that exports it (#4696) + +`build-docs.ts` kept two maps — schema name to category, schema name to page — +keyed by the **bare** schema name, globally. A bare name is not a schema +identity: `build-schemas.ts` publishes `json-schema//.json`, so +the same name under two categories is two published schemas. The docs index now +uses that same `/` key. + +Two things were wrong under the old key, and the second one turned out to be +the bigger of the two: + +- **Same name, two categories, last writer wins.** `ServiceStatus` is an enum + declared in `api/discovery.zod.ts` and an object declared in + `system/core-services.zod.ts`. `system` was walked later, so the API enum was + written to `content/docs/references/api/core-services.mdx` — a page with no + `packages/spec/src/api/core-services.zod.ts` behind it. +- **A re-export was invisible.** The scan matched `export const X` only, so a + name reaching an entry point through `export { XSchema } from '…'` — or a + bare `export { XSchema }` of an imported binding — had no entry for its own + category at all, and fell through to the case above. That accounts for 25 of + the 26 misplaced schemas, not name collisions: `RetryPolicy` under + `./automation` and `./system`, the five `ConnectorInstance*Auth` under + `./integration`, `HttpMethod` / `HttpRequest` under `./api` and `./ui`, the + twelve package-registry RPC envelopes under `./api`, and the metadata-loader + pair under `./system`. + +The index now records every **value** export a `.zod.ts` names — declarations +and re-exports alike, type-only exports excluded because they publish no +`z.ZodType` — and a declaration owns the page over any number of re-exports of +it. Nine pages that named no real file are gone; their sections moved onto the +page of the file that genuinely exports them, which is also the page whose +`Source:` line and `import … from '@objectstack/spec/'` example were +already true: + +| removed page | sections now live on | +| :--- | :--- | +| `api/core-services` | `api/discovery` | +| `api/http` | `api/router` | +| `api/package-registry` | `api/protocol` | +| `automation/retry-policy` | `automation/control-flow` | +| `integration/connector-auth` | `integration/connector` | +| `system/metadata-loader` | `system/metadata-persistence` | +| `system/metadata-types` | `system/metadata-persistence` | +| `system/retry-policy` | `system/job` | +| `ui/http` | `ui/view` | + +Cross-reference links follow the same key: a `$ref` links to the page in the +reader's **own** category when that entry point exports the name, and otherwise +to the single declaring category. When two categories declare the name, no link +is emitted at all — plain text beats a confident link to the wrong schema. + +A name that two files inside one category both claim is now a **build error** +naming both files, never an overwrite. diff --git a/content/docs/references/api/core-services.mdx b/content/docs/references/api/core-services.mdx deleted file mode 100644 index 5f88cac2aa..0000000000 --- a/content/docs/references/api/core-services.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Core Services -description: Core Services protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -## TypeScript Usage - -```typescript -import { ServiceStatus } from '@objectstack/spec/api'; -import type { ServiceStatus } from '@objectstack/spec/api'; - -// Validate data -const result = ServiceStatus.parse(data); -``` - ---- - -## ServiceStatus - -available = fully operational, registered = route declared but handler unverified, unavailable = not installed, degraded = partial, stub = placeholder that returns 501 - -### Allowed Values - -* `available` -* `registered` -* `unavailable` -* `degraded` -* `stub` - - ---- - diff --git a/content/docs/references/api/discovery.mdx b/content/docs/references/api/discovery.mdx index 075a216344..d99d2fbdba 100644 --- a/content/docs/references/api/discovery.mdx +++ b/content/docs/references/api/discovery.mdx @@ -28,8 +28,8 @@ not been verified (may 501 at runtime). ## TypeScript Usage ```typescript -import { ApiRoutesSchema, DiscoverySchema, DiscoveryEnvironmentSchema, RouteHealthEntrySchema, RouteHealthReportSchema, ServiceInfoSchema, ServiceSelfInfoSchema, WellKnownCapabilitiesSchema } from '@objectstack/spec/api'; -import type { ApiRoutes, DiscoveryEnvironment, RouteHealthEntry, RouteHealthReport, ServiceInfo, ServiceSelfInfo, WellKnownCapabilities } from '@objectstack/spec/api'; +import { ApiRoutesSchema, DiscoverySchema, DiscoveryEnvironmentSchema, RouteHealthEntrySchema, RouteHealthReportSchema, ServiceInfoSchema, ServiceSelfInfoSchema, ServiceStatus, WellKnownCapabilitiesSchema } from '@objectstack/spec/api'; +import type { ApiRoutes, DiscoveryEnvironment, RouteHealthEntry, RouteHealthReport, ServiceInfo, ServiceSelfInfo, ServiceStatus, WellKnownCapabilities } from '@objectstack/spec/api'; // Validate data const result = ApiRoutesSchema.parse(data); @@ -157,6 +157,21 @@ Deployment posture a discovery response advertises. Deliberately three coarse bu | **message** | `string` | optional | Human-readable explanation, e.g. what to install for the full implementation | +--- + +## ServiceStatus + +available = fully operational, registered = route declared but handler unverified, unavailable = not installed, degraded = partial, stub = placeholder that returns 501 + +### Allowed Values + +* `available` +* `registered` +* `unavailable` +* `degraded` +* `stub` + + --- ## WellKnownCapabilities diff --git a/content/docs/references/api/http.mdx b/content/docs/references/api/http.mdx deleted file mode 100644 index 6eb5ac2296..0000000000 --- a/content/docs/references/api/http.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Http -description: Http protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -## TypeScript Usage - -```typescript -import { HttpMethod } from '@objectstack/spec/api'; -import type { HttpMethod } from '@objectstack/spec/api'; - -// Validate data -const result = HttpMethod.parse(data); -``` - ---- - -## HttpMethod - -### Allowed Values - -* `GET` -* `POST` -* `PUT` -* `DELETE` -* `PATCH` -* `HEAD` -* `OPTIONS` - - ---- - diff --git a/content/docs/references/api/meta.json b/content/docs/references/api/meta.json index a99c71d5e4..07a1304afe 100644 --- a/content/docs/references/api/meta.json +++ b/content/docs/references/api/meta.json @@ -13,7 +13,6 @@ "versioning", "---Transport & Realtime---", "dispatcher", - "http", "http-cache", "odata", "query-adapter", @@ -26,12 +25,10 @@ "auth", "auth-endpoints", "automation-api", - "core-services", "events", "export", "metadata", "package-api", - "package-registry", "plugin-rest-api", "storage", "---More---", diff --git a/content/docs/references/api/package-registry.mdx b/content/docs/references/api/package-registry.mdx deleted file mode 100644 index 5793e4e5bd..0000000000 --- a/content/docs/references/api/package-registry.mdx +++ /dev/null @@ -1,187 +0,0 @@ ---- -title: Package Registry -description: Package Registry protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -## TypeScript Usage - -```typescript -import { DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema } from '@objectstack/spec/api'; -import type { DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, GetPackageRequest, GetPackageResponse, InstallPackageRequest, InstallPackageResponse, ListPackagesRequest, ListPackagesResponse, UninstallPackageRequest, UninstallPackageResponse } from '@objectstack/spec/api'; - -// Validate data -const result = DisablePackageRequestSchema.parse(data); -``` - ---- - -## DisablePackageRequest - -Disable package request - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **id** | `string` | ✅ | Package ID to disable | - - ---- - -## DisablePackageResponse - -Disable package response - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Disabled package details | -| **message** | `string` | optional | Disable status message | - - ---- - -## EnablePackageRequest - -Enable package request - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **id** | `string` | ✅ | Package ID to enable | - - ---- - -## EnablePackageResponse - -Enable package response - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Enabled package details | -| **message** | `string` | optional | Enable status message | - - ---- - -## GetPackageRequest - -Get package request - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **id** | `string` | ✅ | Package identifier | - - ---- - -## GetPackageResponse - -Get package response - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Package details | - - ---- - -## InstallPackageRequest - -Install package request - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest to install | -| **settings** | `Record` | optional | User-provided settings at install time | -| **enableOnInstall** | `boolean` | optional | Whether to enable immediately after install | -| **platformVersion** | `string` | optional | Current platform version for compatibility verification | - - ---- - -## InstallPackageResponse - -Install package response - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Installed package details | -| **message** | `string` | optional | Installation status message | -| **dependencyResolution** | `{ dependencies: { packageId: string; requiredRange: string; resolvedVersion?: string; installedVersion?: string; … }[]; canProceed: boolean; requiredActions: { type: Enum<'install' \| 'upgrade' \| 'confirm_conflict'>; packageId: string; description: string }[]; installOrder: string[]; … }` | optional | Dependency resolution result from install analysis | - - ---- - -## ListPackagesRequest - -List packages request - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional | Filter by package status | -| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| 'module' \| 'gateway' \| 'adapter'>` | optional | Filter by package type | -| **enabled** | `boolean` | optional | Filter by enabled state | - - ---- - -## ListPackagesResponse - -List packages response - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **packages** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }[]` | ✅ | List of installed packages | -| **total** | `number` | ✅ | Total package count | - - ---- - -## UninstallPackageRequest - -Uninstall package request - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **id** | `string` | ✅ | Package ID to uninstall | - - ---- - -## UninstallPackageResponse - -Uninstall package response - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **id** | `string` | ✅ | Uninstalled package ID | -| **success** | `boolean` | ✅ | Whether uninstall succeeded | -| **message** | `string` | optional | Uninstall status message | - - ---- - diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index cb385e1655..5c3e5795b3 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -20,8 +20,8 @@ validation. Each entry is a canonical `ActionDescriptorSchema`. ## TypeScript Usage ```typescript -import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, CreateViewRequestSchema, CreateViewResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DeleteViewRequestSchema, DeleteViewResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, GetViewRequestSchema, GetViewResponseSchema, HttpFindQueryParamsSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListViewsRequestSchema, ListViewsResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, UpdateViewRequestSchema, UpdateViewResponseSchema } from '@objectstack/spec/api'; -import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse } from '@objectstack/spec/api'; +import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, CreateViewRequestSchema, CreateViewResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DeleteViewRequestSchema, DeleteViewResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, GetViewRequestSchema, GetViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, ListViewsRequestSchema, ListViewsResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, UpdateViewRequestSchema, UpdateViewResponseSchema } from '@objectstack/spec/api'; +import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse } from '@objectstack/spec/api'; // Validate data const result = AiAgentCapabilitiesSchema.parse(data); @@ -520,6 +520,60 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **success** | `boolean` | ✅ | Whether deletion succeeded | +--- + +## DisablePackageRequest + +Disable package request + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Package ID to disable | + + +--- + +## DisablePackageResponse + +Disable package response + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Disabled package details | +| **message** | `string` | optional | Disable status message | + + +--- + +## EnablePackageRequest + +Enable package request + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Package ID to enable | + + +--- + +## EnablePackageResponse + +Enable package response + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Enabled package details | +| **message** | `string` | optional | Enable status message | + + --- ## FindDataRequest @@ -820,6 +874,32 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **fieldPermissions** | `Record` | optional | Field-level permissions keyed by field name | +--- + +## GetPackageRequest + +Get package request + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Package identifier | + + +--- + +## GetPackageResponse + +Get package response + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Package details | + + --- ## GetPresenceRequest @@ -948,6 +1028,37 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **count** | `boolean` | optional | Include total count in response. | +--- + +## InstallPackageRequest + +Install package request + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest to install | +| **settings** | `Record` | optional | User-provided settings at install time | +| **enableOnInstall** | `boolean` | optional | Whether to enable immediately after install | +| **platformVersion** | `string` | optional | Current platform version for compatibility verification | + + +--- + +## InstallPackageResponse + +Install package response + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Installed package details | +| **message** | `string` | optional | Installation status message | +| **dependencyResolution** | `{ dependencies: { packageId: string; requiredRange: string; resolvedVersion?: string; installedVersion?: string; … }[]; canProceed: boolean; requiredActions: { type: Enum<'install' \| 'upgrade' \| 'confirm_conflict'>; packageId: string; description: string }[]; installOrder: string[]; … }` | optional | Dependency resolution result from install analysis | + + --- ## ListAiConversationsRequest @@ -1024,6 +1135,35 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **cursor** | `string` | optional | Next page cursor | +--- + +## ListPackagesRequest + +List packages request + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional | Filter by package status | +| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| 'module' \| 'gateway' \| 'adapter'>` | optional | Filter by package type | +| **enabled** | `boolean` | optional | Filter by enabled state | + + +--- + +## ListPackagesResponse + +List packages response + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **packages** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }[]` | ✅ | List of installed packages | +| **total** | `number` | ✅ | Total package count | + + --- ## ListViewsRequest @@ -1307,6 +1447,34 @@ const result = AiAgentCapabilitiesSchema.parse(data); | **success** | `boolean` | ✅ | Whether presence was set | +--- + +## UninstallPackageRequest + +Uninstall package request + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Package ID to uninstall | + + +--- + +## UninstallPackageResponse + +Uninstall package response + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | Uninstalled package ID | +| **success** | `boolean` | ✅ | Whether uninstall succeeded | +| **message** | `string` | optional | Uninstall status message | + + --- ## UnregisterDeviceRequest diff --git a/content/docs/references/api/router.mdx b/content/docs/references/api/router.mdx index 93403dff6a..9a996d7916 100644 --- a/content/docs/references/api/router.mdx +++ b/content/docs/references/api/router.mdx @@ -16,8 +16,8 @@ Classifies routes for middleware application and security policies. ## TypeScript Usage ```typescript -import { ConflictResolutionStrategy, RouteCategory, RouteDefinitionSchema, RouterConfigSchema } from '@objectstack/spec/api'; -import type { ConflictResolutionStrategy, RouteCategory, RouteDefinition, RouterConfig } from '@objectstack/spec/api'; +import { ConflictResolutionStrategy, HttpMethod, RouteCategory, RouteDefinitionSchema, RouterConfigSchema } from '@objectstack/spec/api'; +import type { ConflictResolutionStrategy, HttpMethod, RouteCategory, RouteDefinition, RouterConfig } from '@objectstack/spec/api'; // Validate data const result = ConflictResolutionStrategy.parse(data); @@ -35,6 +35,21 @@ const result = ConflictResolutionStrategy.parse(data); * `last-wins` +--- + +## HttpMethod + +### Allowed Values + +* `GET` +* `POST` +* `PUT` +* `DELETE` +* `PATCH` +* `HEAD` +* `OPTIONS` + + --- ## RouteCategory diff --git a/content/docs/references/automation/control-flow.mdx b/content/docs/references/automation/control-flow.mdx index fa0e5001e2..ee7173642d 100644 --- a/content/docs/references/automation/control-flow.mdx +++ b/content/docs/references/automation/control-flow.mdx @@ -118,8 +118,8 @@ for is untouched, and it simply stopped silently repairing its own input. ## TypeScript Usage ```typescript -import { FlowRegionSchema, LoopConfigSchema, ParallelBranchSchema, ParallelConfigSchema, TryCatchConfigSchema } from '@objectstack/spec/automation'; -import type { FlowRegion, LoopConfig, ParallelBranch, ParallelConfig, TryCatchConfig } from '@objectstack/spec/automation'; +import { FlowRegionSchema, LoopConfigSchema, ParallelBranchSchema, ParallelConfigSchema, RetryPolicySchema, TryCatchConfigSchema } from '@objectstack/spec/automation'; +import type { FlowRegion, LoopConfig, ParallelBranch, ParallelConfig, RetryPolicy, TryCatchConfig } from '@objectstack/spec/automation'; // Validate data const result = FlowRegionSchema.parse(data); @@ -176,6 +176,22 @@ const result = FlowRegionSchema.parse(data); | **branches** | `{ name?: string; nodes: { id: string; type: string; label: string; config?: Record; … }[]; edges?: { id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[] }[]` | ✅ | Branch regions executed concurrently; implicit join at block end | +--- + +## RetryPolicy + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxRetries** | `integer` | ✅ | Retry attempts after the initial one. 0 (the default) means no retry — state a count to opt in. | +| **backoffMs** | `integer` | ✅ | Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier | +| **backoffMultiplier** | `number` | ✅ | Exponential backoff multiplier; 1 (the default) keeps the delay flat | +| **maxRetryDelayMs** | `integer` | ✅ | Ceiling for a single backoff delay (ms) | +| **jitter** | `boolean` | ✅ | Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries | +| **retryDelayMs** | `any` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry`, `flow.errorHandling` and an ETL pipeline's `retry`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. `os migrate meta --from 16` rewrites it for you. | + + --- ## TryCatchConfig diff --git a/content/docs/references/automation/meta.json b/content/docs/references/automation/meta.json index 57ef1974b1..2ee557f996 100644 --- a/content/docs/references/automation/meta.json +++ b/content/docs/references/automation/meta.json @@ -18,7 +18,6 @@ "builtin-node-config", "flow-function", "io-node-config", - "retry-policy", "schemaless-node-config" ] } \ No newline at end of file diff --git a/content/docs/references/automation/retry-policy.mdx b/content/docs/references/automation/retry-policy.mdx deleted file mode 100644 index 8c3eb418e5..0000000000 --- a/content/docs/references/automation/retry-policy.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Retry Policy -description: Retry Policy protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -## TypeScript Usage - -```typescript -import { RetryPolicySchema } from '@objectstack/spec/automation'; -import type { RetryPolicy } from '@objectstack/spec/automation'; - -// Validate data -const result = RetryPolicySchema.parse(data); -``` - ---- - -## RetryPolicy - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **maxRetries** | `integer` | ✅ | Retry attempts after the initial one. 0 (the default) means no retry — state a count to opt in. | -| **backoffMs** | `integer` | ✅ | Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier | -| **backoffMultiplier** | `number` | ✅ | Exponential backoff multiplier; 1 (the default) keeps the delay flat | -| **maxRetryDelayMs** | `integer` | ✅ | Ceiling for a single backoff delay (ms) | -| **jitter** | `boolean` | ✅ | Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries | -| **retryDelayMs** | `any` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry`, `flow.errorHandling` and an ETL pipeline's `retry`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. `os migrate meta --from 16` rewrites it for you. | - - ---- - diff --git a/content/docs/references/integration/connector-auth.mdx b/content/docs/references/integration/connector-auth.mdx deleted file mode 100644 index 3831efe454..0000000000 --- a/content/docs/references/integration/connector-auth.mdx +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Connector Auth -description: Connector Auth protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -## TypeScript Usage - -```typescript -import { ConnectorInstanceAPIKeyAuthSchema, ConnectorInstanceAuthSchema, ConnectorInstanceBasicAuthSchema, ConnectorInstanceBearerAuthSchema, ConnectorInstanceNoAuthSchema } from '@objectstack/spec/integration'; -import type { ConnectorInstanceAuth } from '@objectstack/spec/integration'; - -// Validate data -const result = ConnectorInstanceAPIKeyAuthSchema.parse(data); -``` - ---- - -## ConnectorInstanceAPIKeyAuth - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `'api-key'` | ✅ | | -| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the API key at materialization. Never an inline key. | -| **headerName** | `string` | optional | HTTP header carrying the key (default X-API-Key). | -| **paramName** | `string` | optional | Query parameter carrying the key (alternative to header). | - - ---- - -## ConnectorInstanceAuth - -### Union Options - -This schema accepts one of the following structures: - -#### Option 1 - -**Type:** `none` - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `'none'` | ✅ | | - ---- - -#### Option 2 - -**Type:** `bearer` - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `'bearer'` | ✅ | | -| **credentialRef** | `string` | ✅ | Secrets-layer reference (e.g. an env-var name in the open tier) resolved to the bearer token at materialization. Never an inline token. | - ---- - -#### Option 3 - -**Type:** `api-key` - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `'api-key'` | ✅ | | -| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the API key at materialization. Never an inline key. | -| **headerName** | `string` | optional | HTTP header carrying the key (default X-API-Key). | -| **paramName** | `string` | optional | Query parameter carrying the key (alternative to header). | - ---- - -#### Option 4 - -**Type:** `basic` - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `'basic'` | ✅ | | -| **username** | `string` | ✅ | Username (not a secret; safe to keep in metadata). | -| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the password at materialization. Never an inline password. | - ---- - - ---- - -## ConnectorInstanceBasicAuth - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `'basic'` | ✅ | | -| **username** | `string` | ✅ | Username (not a secret; safe to keep in metadata). | -| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the password at materialization. Never an inline password. | - - ---- - -## ConnectorInstanceBearerAuth - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `'bearer'` | ✅ | | -| **credentialRef** | `string` | ✅ | Secrets-layer reference (e.g. an env-var name in the open tier) resolved to the bearer token at materialization. Never an inline token. | - - ---- - -## ConnectorInstanceNoAuth - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `'none'` | ✅ | | - - ---- - diff --git a/content/docs/references/integration/connector.mdx b/content/docs/references/integration/connector.mdx index dfeadc1cd3..5a47d19f7a 100644 --- a/content/docs/references/integration/connector.mdx +++ b/content/docs/references/integration/connector.mdx @@ -134,8 +134,8 @@ a dead end of the same class in #4738.) ## TypeScript Usage ```typescript -import { CircuitBreakerConfigSchema, ConnectorSchema, ConnectorActionSchema, ConnectorConflictResolutionSchema, ConnectorErrorCategorySchema, ConnectorFieldMappingSchema, ConnectorHealthSchema, ConnectorRetryStrategySchema, ConnectorStatusSchema, ConnectorTriggerSchema, ConnectorTypeSchema, DataSyncConfigSchema, DeclarativeConnectorEntrySchema, ErrorMappingConfigSchema, ErrorMappingRuleSchema, HealthCheckConfigSchema, RetryConfigSchema, SyncStrategySchema, WebhookConfigSchema, WebhookEventSchema, WebhookSignatureAlgorithmSchema } from '@objectstack/spec/integration'; -import type { CircuitBreakerConfig, Connector, ConnectorConflictResolution, ConnectorErrorCategory, ConnectorFieldMapping, ConnectorHealth, ConnectorRetryStrategy, ConnectorStatus, ConnectorType, DataSyncConfig, DeclarativeConnectorEntry, ErrorMappingConfig, ErrorMappingRule, HealthCheckConfig, RetryConfig, SyncStrategy, WebhookConfig, WebhookEvent, WebhookSignatureAlgorithm } from '@objectstack/spec/integration'; +import { CircuitBreakerConfigSchema, ConnectorSchema, ConnectorActionSchema, ConnectorConflictResolutionSchema, ConnectorErrorCategorySchema, ConnectorFieldMappingSchema, ConnectorHealthSchema, ConnectorInstanceAPIKeyAuthSchema, ConnectorInstanceAuthSchema, ConnectorInstanceBasicAuthSchema, ConnectorInstanceBearerAuthSchema, ConnectorInstanceNoAuthSchema, ConnectorRetryStrategySchema, ConnectorStatusSchema, ConnectorTriggerSchema, ConnectorTypeSchema, DataSyncConfigSchema, DeclarativeConnectorEntrySchema, ErrorMappingConfigSchema, ErrorMappingRuleSchema, HealthCheckConfigSchema, RetryConfigSchema, SyncStrategySchema, WebhookConfigSchema, WebhookEventSchema, WebhookSignatureAlgorithmSchema } from '@objectstack/spec/integration'; +import type { CircuitBreakerConfig, Connector, ConnectorConflictResolution, ConnectorErrorCategory, ConnectorFieldMapping, ConnectorHealth, ConnectorInstanceAuth, ConnectorRetryStrategy, ConnectorStatus, ConnectorType, DataSyncConfig, DeclarativeConnectorEntry, ErrorMappingConfig, ErrorMappingRule, HealthCheckConfig, RetryConfig, SyncStrategy, WebhookConfig, WebhookEvent, WebhookSignatureAlgorithm } from '@objectstack/spec/integration'; // Validate data const result = CircuitBreakerConfigSchema.parse(data); @@ -270,6 +270,119 @@ Connector health configuration | **circuitBreaker** | `{ enabled: boolean; failureThreshold: number; resetTimeoutMs: number; halfOpenMaxRequests: number; … }` | optional | Circuit breaker configuration | +--- + +## ConnectorInstanceAPIKeyAuth + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'api-key'` | ✅ | | +| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the API key at materialization. Never an inline key. | +| **headerName** | `string` | optional | HTTP header carrying the key (default X-API-Key). | +| **paramName** | `string` | optional | Query parameter carrying the key (alternative to header). | + + +--- + +## ConnectorInstanceAuth + +### Union Options + +This schema accepts one of the following structures: + +#### Option 1 + +**Type:** `none` + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'none'` | ✅ | | + +--- + +#### Option 2 + +**Type:** `bearer` + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'bearer'` | ✅ | | +| **credentialRef** | `string` | ✅ | Secrets-layer reference (e.g. an env-var name in the open tier) resolved to the bearer token at materialization. Never an inline token. | + +--- + +#### Option 3 + +**Type:** `api-key` + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'api-key'` | ✅ | | +| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the API key at materialization. Never an inline key. | +| **headerName** | `string` | optional | HTTP header carrying the key (default X-API-Key). | +| **paramName** | `string` | optional | Query parameter carrying the key (alternative to header). | + +--- + +#### Option 4 + +**Type:** `basic` + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'basic'` | ✅ | | +| **username** | `string` | ✅ | Username (not a secret; safe to keep in metadata). | +| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the password at materialization. Never an inline password. | + +--- + + +--- + +## ConnectorInstanceBasicAuth + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'basic'` | ✅ | | +| **username** | `string` | ✅ | Username (not a secret; safe to keep in metadata). | +| **credentialRef** | `string` | ✅ | Secrets-layer reference resolved to the password at materialization. Never an inline password. | + + +--- + +## ConnectorInstanceBearerAuth + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'bearer'` | ✅ | | +| **credentialRef** | `string` | ✅ | Secrets-layer reference (e.g. an env-var name in the open tier) resolved to the bearer token at materialization. Never an inline token. | + + +--- + +## ConnectorInstanceNoAuth + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **type** | `'none'` | ✅ | | + + --- ## ConnectorRetryStrategy diff --git a/content/docs/references/integration/meta.json b/content/docs/references/integration/meta.json index 4fc44358aa..fb8d485502 100644 --- a/content/docs/references/integration/meta.json +++ b/content/docs/references/integration/meta.json @@ -2,7 +2,6 @@ "title": "Integration Protocol", "pages": [ "---Connectors---", - "connector", - "connector-auth" + "connector" ] } \ No newline at end of file diff --git a/content/docs/references/system/job.mdx b/content/docs/references/system/job.mdx index a12e00cfcf..7c40b68b69 100644 --- a/content/docs/references/system/job.mdx +++ b/content/docs/references/system/job.mdx @@ -16,8 +16,8 @@ Schedule jobs using cron expressions ## TypeScript Usage ```typescript -import { CronScheduleSchema, IntervalScheduleSchema, JobSchema, JobExecutionSchema, JobExecutionStatus, OnceScheduleSchema, ScheduleSchema } from '@objectstack/spec/system'; -import type { CronSchedule, IntervalSchedule, Job, JobExecution, JobExecutionStatus, OnceSchedule, Schedule } from '@objectstack/spec/system'; +import { CronScheduleSchema, IntervalScheduleSchema, JobSchema, JobExecutionSchema, JobExecutionStatus, OnceScheduleSchema, RetryPolicySchema, ScheduleSchema } from '@objectstack/spec/system'; +import type { CronSchedule, IntervalSchedule, Job, JobExecution, JobExecutionStatus, OnceSchedule, RetryPolicy, Schedule } from '@objectstack/spec/system'; // Validate data const result = CronScheduleSchema.parse(data); @@ -113,6 +113,22 @@ const result = CronScheduleSchema.parse(data); | **at** | `string` | ✅ | ISO 8601 datetime when to execute | +--- + +## RetryPolicy + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **maxRetries** | `integer` | ✅ | Retry attempts after the initial one. 0 (the default) means no retry — state a count to opt in. | +| **backoffMs** | `integer` | ✅ | Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier | +| **backoffMultiplier** | `number` | ✅ | Exponential backoff multiplier; 1 (the default) keeps the delay flat | +| **maxRetryDelayMs** | `integer` | ✅ | Ceiling for a single backoff delay (ms) | +| **jitter** | `boolean` | ✅ | Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries | +| **retryDelayMs** | `any` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry`, `flow.errorHandling` and an ETL pipeline's `retry`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. `os migrate meta --from 16` rewrites it for you. | + + --- ## Schedule diff --git a/content/docs/references/system/meta.json b/content/docs/references/system/meta.json index e07635dbfa..fbff8d1e47 100644 --- a/content/docs/references/system/meta.json +++ b/content/docs/references/system/meta.json @@ -20,7 +20,6 @@ "http-server", "job", "message-queue", - "metadata-loader", "metadata-persistence", "notification", "object-storage", @@ -44,8 +43,6 @@ "collaboration", "doc", "---More---", - "metadata-types", - "retry-policy", "stack-server" ] } \ No newline at end of file diff --git a/content/docs/references/system/metadata-loader.mdx b/content/docs/references/system/metadata-loader.mdx deleted file mode 100644 index 58f3acf653..0000000000 --- a/content/docs/references/system/metadata-loader.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Metadata Loader -description: Metadata Loader protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -## TypeScript Usage - -```typescript -import { MetadataFallbackStrategySchema, MetadataManagerConfigSchema } from '@objectstack/spec/system'; -import type { MetadataFallbackStrategy, MetadataManagerConfig } from '@objectstack/spec/system'; - -// Validate data -const result = MetadataFallbackStrategySchema.parse(data); -``` - ---- - -## MetadataFallbackStrategy - -### Allowed Values - -* `filesystem` -* `memory` -* `none` - - ---- - -## MetadataManagerConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **datasource** | `string` | optional | Datasource name reference for database persistence | -| **tableName** | `string` | ✅ | Database table name for metadata storage | -| **fallback** | `Enum<'filesystem' \| 'memory' \| 'none'>` | ✅ | Fallback strategy when datasource is unavailable | -| **rootDir** | `string` | optional | Root directory path | -| **formats** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>[]` | ✅ | Enabled formats | -| **cache** | `{ enabled: boolean; ttl: integer; maxSize?: integer; databaseLoader?: object }` | optional | Cache settings | -| **watch** | `boolean` | ✅ | Enable file watching | -| **watchOptions** | `{ ignored?: string[]; persistent: boolean; ignoreInitial: boolean }` | optional | File watcher options | -| **validation** | `{ strict: boolean; throwOnError: boolean }` | optional | Validation settings | -| **loaderOptions** | `Record` | optional | Loader-specific configuration | -| **persistence** | `{ writable: boolean; overlayWritable: boolean }` | optional | Persistence write gates | - - ---- - diff --git a/content/docs/references/system/metadata-persistence.mdx b/content/docs/references/system/metadata-persistence.mdx index bf6e5af53a..331b5fe235 100644 --- a/content/docs/references/system/metadata-persistence.mdx +++ b/content/docs/references/system/metadata-persistence.mdx @@ -16,8 +16,8 @@ Defines the lifecycle and mutability of a metadata item. ## TypeScript Usage ```typescript -import { MetadataCollectionInfoSchema, MetadataDiffResultSchema, MetadataHistoryQueryOptionsSchema, MetadataHistoryQueryResultSchema, MetadataHistoryRecordSchema, MetadataHistoryRetentionPolicySchema, MetadataLoadOptionsSchema, MetadataLoadResultSchema, MetadataLoaderContractSchema, MetadataRecordSchema, MetadataSaveOptionsSchema, MetadataSaveResultSchema, MetadataScopeSchema, MetadataSourceSchema, MetadataStateSchema, MetadataStatsSchema, MetadataWatchEventSchema, PackagePublishResultSchema } from '@objectstack/spec/system'; -import type { MetadataCollectionInfo, MetadataDiffResult, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system'; +import { MetadataCollectionInfoSchema, MetadataDiffResultSchema, MetadataFallbackStrategySchema, MetadataFormatSchema, MetadataHistoryQueryOptionsSchema, MetadataHistoryQueryResultSchema, MetadataHistoryRecordSchema, MetadataHistoryRetentionPolicySchema, MetadataLoadOptionsSchema, MetadataLoadResultSchema, MetadataLoaderContractSchema, MetadataManagerConfigSchema, MetadataRecordSchema, MetadataSaveOptionsSchema, MetadataSaveResultSchema, MetadataScopeSchema, MetadataSourceSchema, MetadataStateSchema, MetadataStatsSchema, MetadataWatchEventSchema, PackagePublishResultSchema } from '@objectstack/spec/system'; +import type { MetadataCollectionInfo, MetadataDiffResult, MetadataFallbackStrategy, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system'; // Validate data const result = MetadataCollectionInfoSchema.parse(data); @@ -55,6 +55,31 @@ const result = MetadataCollectionInfoSchema.parse(data); | **summary** | `string` | optional | Human-readable summary of changes | +--- + +## MetadataFallbackStrategy + +### Allowed Values + +* `filesystem` +* `memory` +* `none` + + +--- + +## MetadataFormat + +Metadata file format + +### Allowed Values + +* `yaml` +* `json` +* `typescript` +* `javascript` + + --- ## MetadataHistoryQueryOptions @@ -178,6 +203,27 @@ const result = MetadataCollectionInfoSchema.parse(data); | **capabilities** | `{ read: boolean; write: boolean; watch: boolean; list: boolean }` | ✅ | | +--- + +## MetadataManagerConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **datasource** | `string` | optional | Datasource name reference for database persistence | +| **tableName** | `string` | ✅ | Database table name for metadata storage | +| **fallback** | `Enum<'filesystem' \| 'memory' \| 'none'>` | ✅ | Fallback strategy when datasource is unavailable | +| **rootDir** | `string` | optional | Root directory path | +| **formats** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>[]` | ✅ | Enabled formats | +| **cache** | `{ enabled: boolean; ttl: integer; maxSize?: integer; databaseLoader?: object }` | optional | Cache settings | +| **watch** | `boolean` | ✅ | Enable file watching | +| **watchOptions** | `{ ignored?: string[]; persistent: boolean; ignoreInitial: boolean }` | optional | File watcher options | +| **validation** | `{ strict: boolean; throwOnError: boolean }` | optional | Validation settings | +| **loaderOptions** | `Record` | optional | Loader-specific configuration | +| **persistence** | `{ writable: boolean; overlayWritable: boolean }` | optional | Persistence write gates | + + --- ## MetadataRecord diff --git a/content/docs/references/system/metadata-types.mdx b/content/docs/references/system/metadata-types.mdx deleted file mode 100644 index 6a60ce92ee..0000000000 --- a/content/docs/references/system/metadata-types.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Metadata Types -description: Metadata Types protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -## TypeScript Usage - -```typescript -import { MetadataFormatSchema } from '@objectstack/spec/system'; -import type { MetadataFormat } from '@objectstack/spec/system'; - -// Validate data -const result = MetadataFormatSchema.parse(data); -``` - ---- - -## MetadataFormat - -Metadata file format - -### Allowed Values - -* `yaml` -* `json` -* `typescript` -* `javascript` - - ---- - diff --git a/content/docs/references/system/retry-policy.mdx b/content/docs/references/system/retry-policy.mdx deleted file mode 100644 index fbcd410ded..0000000000 --- a/content/docs/references/system/retry-policy.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: Retry Policy -description: Retry Policy protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -## TypeScript Usage - -```typescript -import { RetryPolicySchema } from '@objectstack/spec/system'; -import type { RetryPolicy } from '@objectstack/spec/system'; - -// Validate data -const result = RetryPolicySchema.parse(data); -``` - ---- - -## RetryPolicy - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **maxRetries** | `integer` | ✅ | Retry attempts after the initial one. 0 (the default) means no retry — state a count to opt in. | -| **backoffMs** | `integer` | ✅ | Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier | -| **backoffMultiplier** | `number` | ✅ | Exponential backoff multiplier; 1 (the default) keeps the delay flat | -| **maxRetryDelayMs** | `integer` | ✅ | Ceiling for a single backoff delay (ms) | -| **jitter** | `boolean` | ✅ | Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries | -| **retryDelayMs** | `any` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry`, `flow.errorHandling` and an ETL pipeline's `retry`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. `os migrate meta --from 16` rewrites it for you. | - - ---- - diff --git a/content/docs/references/ui/http.mdx b/content/docs/references/ui/http.mdx deleted file mode 100644 index 0d9c021cec..0000000000 --- a/content/docs/references/ui/http.mdx +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Http -description: Http protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -## TypeScript Usage - -```typescript -import { HttpMethodSchema, HttpRequestSchema } from '@objectstack/spec/ui'; -import type { HttpRequest } from '@objectstack/spec/ui'; - -// Validate data -const result = HttpMethodSchema.parse(data); -``` - ---- - -## HttpMethod - -### Allowed Values - -* `GET` -* `POST` -* `PUT` -* `PATCH` -* `DELETE` - - ---- - -## HttpRequest - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **url** | `string` | ✅ | API endpoint URL | -| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>` | ✅ | HTTP method | -| **headers** | `Record` | optional | Custom HTTP headers | -| **params** | `Record` | optional | Query parameters | -| **body** | `any` | optional | Request body for POST/PUT/PATCH | - - ---- - diff --git a/content/docs/references/ui/meta.json b/content/docs/references/ui/meta.json index 4698949bbd..a1dff05a94 100644 --- a/content/docs/references/ui/meta.json +++ b/content/docs/references/ui/meta.json @@ -18,7 +18,6 @@ "responsive", "theme", "---Platform---", - "http", "i18n", "notification", "sharing", diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 97d6176430..d36eb4cd69 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -16,8 +16,8 @@ Migrated to [shared/http.zod.ts](/docs/references/shared/http). Re-exported here ## TypeScript Usage ```typescript -import { AddRecordConfigSchema, AppearanceConfigSchema, CalendarConfigSchema, ColumnPrefixSchema, ColumnSummarySchema, ColumnSummaryConfigSchema, FormButtonConfigSchema, FormFieldSchema, FormSectionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListViewSchema, NavigationConfigSchema, NavigationModeSchema, ObjectListViewSchema, ObjectUserFiltersSchema, PaginationConfigSchema, RowColorConfigSchema, RowHeightSchema, SelectionConfigSchema, TimelineConfigSchema, TreeConfigSchema, UserActionsConfigSchema, UserFilterFieldSchema, UserFiltersSchema, ViewSchema, ViewDataSchema, ViewFilterRuleSchema, ViewItemSchema, ViewItemNameSchema, ViewItemWireSchema, ViewKindSchema, ViewScopeSchema, ViewSharingSchema, ViewTabSchema, VisualizationTypeSchema } from '@objectstack/spec/ui'; -import type { AddRecordConfig, AppearanceConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, FormButtonConfig, FormField, FormSection, FormView, GalleryConfig, GroupingConfig, ListChartConfig, ListColumn, ListView, NavigationConfig, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemWire, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui'; +import { AddRecordConfigSchema, AppearanceConfigSchema, CalendarConfigSchema, ColumnPrefixSchema, ColumnSummarySchema, ColumnSummaryConfigSchema, FormButtonConfigSchema, FormFieldSchema, FormSectionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, HttpMethodSchema, HttpRequestSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListViewSchema, NavigationConfigSchema, NavigationModeSchema, ObjectListViewSchema, ObjectUserFiltersSchema, PaginationConfigSchema, RowColorConfigSchema, RowHeightSchema, SelectionConfigSchema, TimelineConfigSchema, TreeConfigSchema, UserActionsConfigSchema, UserFilterFieldSchema, UserFiltersSchema, ViewSchema, ViewDataSchema, ViewFilterRuleSchema, ViewItemSchema, ViewItemNameSchema, ViewItemWireSchema, ViewKindSchema, ViewScopeSchema, ViewSharingSchema, ViewTabSchema, VisualizationTypeSchema } from '@objectstack/spec/ui'; +import type { AddRecordConfig, AppearanceConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, FormButtonConfig, FormField, FormSection, FormView, GalleryConfig, GroupingConfig, HttpRequest, ListChartConfig, ListColumn, ListView, NavigationConfig, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemWire, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui'; // Validate data const result = AddRecordConfigSchema.parse(data); @@ -305,6 +305,34 @@ Record grouping configuration | **collapsed** | `boolean` | ✅ | Collapse groups by default | +--- + +## HttpMethod + +### Allowed Values + +* `GET` +* `POST` +* `PUT` +* `PATCH` +* `DELETE` + + +--- + +## HttpRequest + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **url** | `string` | ✅ | API endpoint URL | +| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'>` | ✅ | HTTP method | +| **headers** | `Record` | optional | Custom HTTP headers | +| **params** | `Record` | optional | Query parameters | +| **body** | `any` | optional | Request body for POST/PUT/PATCH | + + --- ## KanbanConfig diff --git a/packages/spec/scripts/build-docs.ts b/packages/spec/scripts/build-docs.ts index e78c5d9a1b..b503c7d3d0 100644 --- a/packages/spec/scripts/build-docs.ts +++ b/packages/spec/scripts/build-docs.ts @@ -29,6 +29,13 @@ import { import { escapeMdxDescription } from './lib/escape-mdx'; import { anchorFor, formatType, type TypeContext } from './lib/format-type'; import { createSink } from './lib/generated-output'; +import { + buildSchemaIndex, + formatConflicts, + resolveSchemaPage, + type SchemaIndex, + type ZodFileInput, +} from './lib/schema-index'; import { schemaNameFromExportKey } from './lib/schema-name'; const SCHEMA_DIR = path.resolve(__dirname, '../json-schema'); @@ -66,14 +73,8 @@ const CATEGORIES = fs.readdirSync(SRC_DIR) return acc; }, {} as Record); -// Map SchemaName -> Category (e.g. 'Object' -> 'data') -const schemaCategoryMap = new Map(); -// Map SchemaName -> Zod file (e.g. 'Object' -> 'object') -const schemaZodFileMap = new Map(); // Track all zod files per category const categoryZodFiles = new Map>(); -// Track Zod File collisions -const zodFileCounts = new Map(); /** * Page slug -> its real path under `packages/spec/src//`. * @@ -111,7 +112,9 @@ function collectZodFiles(dir: string, prefix = ''): Array<{ slug: string; rel: s } // Scan source files to build maps -function scanCategories() { +function scanCategories(): SchemaIndex { + const files: ZodFileInput[] = []; + Object.keys(CATEGORIES).forEach(category => { const dir = path.join(SRC_DIR, category); if (!fs.existsSync(dir)) return; @@ -121,27 +124,29 @@ function scanCategories() { for (const { slug, rel } of collectZodFiles(dir)) { zodFiles.add(slug); zodFileSourceRel.set(`${category}/${slug}`, rel); - - const count = zodFileCounts.get(slug) || 0; - zodFileCounts.set(slug, count + 1); - - const content = fs.readFileSync(path.join(dir, rel), 'utf-8'); - - // Match export const Name = ... OR export const Name: Type = ... - const regex = /export const (\w+)\s*(?:[:=])/g; - - let match; - while ((match = regex.exec(content)) !== null) { - const rawName = match[1]; - // Suffix-only strip — shared with build-schemas.ts; see lib/schema-name.ts (#4592). - const finalName = schemaNameFromExportKey(rawName); - schemaCategoryMap.set(finalName, category); - schemaZodFileMap.set(finalName, slug); - } + files.push({ + category, + slug, + rel, + source: fs.readFileSync(path.join(dir, rel), 'utf-8'), + }); } categoryZodFiles.set(category, zodFiles); }); + + // Suffix-only strip — shared with build-schemas.ts; see lib/schema-name.ts (#4592). + const index = buildSchemaIndex(files, schemaNameFromExportKey); + + // Two files in one category laying equal claim to a name is not a state this + // generator may resolve: whichever it picked, the loser's schema would be + // written onto a page named after a file that does not contain it. Stop. + if (index.conflicts.length > 0) { + console.error(`\n✗ ${formatConflicts(index.conflicts)}`); + process.exit(1); + } + + return index; } /** @@ -154,7 +159,14 @@ function sourcePathFor(category: string, zodFile: string): string | undefined { return rel ? `packages/spec/src/${category}/${rel}` : undefined; } -scanCategories(); +/** + * `/` -> the page that documents it, plus the link rules + * over it. Keyed by category on purpose: `build-schemas.ts` publishes + * `json-schema//.json`, so the same name under two categories + * is two published schemas and must be two index entries (#4696). See + * `lib/schema-index.ts` for why a re-export counts and which file wins. + */ +const schemaIndex = scanCategories(); // ── Import examples: the package's real export surface ─────────────────────── // `api-surface.json` is the committed record of every `name (kind)` per public @@ -183,15 +195,21 @@ const IMPORT_BASELINE_COMMENT = 'tsx scripts/build-docs.ts --update-import-baseline (after gen:schema).'; /** - * Resolve a schema name to its page. Returns null when the schema isn't one we - * generate a page for — callers then render the type without a link rather than - * emitting a 404. + * Resolve a schema name to its page, AS SEEN FROM the category being rendered. + * Returns null when the schema isn't one we generate a page for, or when the + * name alone does not identify one — callers then render the type without a + * link rather than emitting a 404 or a confident link to the wrong schema. + * + * The `fromCategory` argument is the whole point: a bare name is not a schema + * identity (#4696). `ServiceStatus` is an `api` enum AND a `system` object, and + * a single global lookup answered both with whichever the directory walk + * reached last. */ -function schemaHref(name: string): string | null { - const category = schemaCategoryMap.get(name); - const zodFile = schemaZodFileMap.get(name); - if (!category || !zodFile) return null; - return `/docs/references/${category}/${zodFile}${anchorFor(name)}`; +function schemaHrefFrom(fromCategory: string): (name: string) => string | null { + return (name: string) => { + const page = resolveSchemaPage(schemaIndex, fromCategory, name); + return page ? `/docs/references/${page.category}/${page.slug}${anchorFor(name)}` : null; + }; } @@ -266,7 +284,7 @@ function generateMarkdown(schemaName: string, schema: any, category: string, zod md += `${escapeMdxDescription(mainDef.description)}\n\n`; } - const typeCtx: TypeContext = { defs, currentSchema: schemaName, schemaHref }; + const typeCtx: TypeContext = { defs, currentSchema: schemaName, schemaHref: schemaHrefFrom(category) }; const renderProperties = (props: any, required: Set = new Set()) => { let t = `### Properties\n\n`; @@ -408,10 +426,17 @@ const SECTION_GROUPS: Record { section: 'Knowledge & RAG', pages: ['knowledge-document', 'knowledge-source', 'embedding'] }, { section: 'Models & Runtime', pages: ['model-registry', 'conversation', 'usage'] }, ], + // `http` / `core-services` / `package-registry` left this category at #4696: + // all three were pages `api/` never had a `.zod.ts` for — the bare-name index + // borrowed another category's slug for a re-exported or same-named schema. + // Their sections moved to the api file that really exports them (`router`, + // `discovery`, `protocol`). `buildCategoryPages` filters by what was emitted, + // so leaving the names here would have been silently harmless — which is why + // they are removed deliberately instead (same discipline as #4988 below). api: [ { section: 'Contract & Routing', pages: ['protocol', 'contract', 'endpoint', 'router', 'registry', 'discovery', 'documentation', 'versioning', 'errors', 'batch'] }, - { section: 'Transport & Realtime', pages: ['http', 'http-cache', 'rest-server', 'websocket', 'realtime', 'realtime-shared', 'odata', 'query-adapter', 'dispatcher'] }, - { section: 'Service APIs', pages: ['core-services', 'auth', 'auth-endpoints', 'identity', 'metadata', 'metadata-plugin', 'automation-api', 'analytics', 'export', 'storage', 'notification', 'events', 'connector', 'package-api', 'package-registry', 'plugin-rest-api'] }, + { section: 'Transport & Realtime', pages: ['http-cache', 'rest-server', 'websocket', 'realtime', 'realtime-shared', 'odata', 'query-adapter', 'dispatcher'] }, + { section: 'Service APIs', pages: ['auth', 'auth-endpoints', 'identity', 'metadata', 'metadata-plugin', 'automation-api', 'analytics', 'export', 'storage', 'notification', 'events', 'connector', 'package-api', 'plugin-rest-api'] }, ], automation: [ { section: 'Flow & Execution', pages: ['flow', 'control-flow', 'execution', 'node-executor', 'state-machine', 'time-relative-trigger'] }, @@ -430,7 +455,10 @@ const SECTION_GROUPS: Record { section: 'Documents & Seed', pages: ['document', 'seed', 'seed-loader', 'feed'] }, ], integration: [ - { section: 'Connectors', pages: ['connector', 'connector-auth', 'mapping', 'translation'] }, + // `connector-auth` removed at #4696 — `integration/` has no such file; the + // five `ConnectorInstance*Auth` schemas reach this entry point through + // `integration/connector.zod.ts`, and are documented there now. + { section: 'Connectors', pages: ['connector', 'mapping', 'translation'] }, { section: 'Transport & Storage', pages: ['http', 'message-queue', 'object-storage', 'offline'] }, { section: 'Tenancy', pages: ['tenant', 'misc'] }, ], @@ -442,7 +470,10 @@ const SECTION_GROUPS: Record ], system: [ { section: 'Config & Settings', pages: ['settings-manifest', 'settings-client', 'registry-config', 'auth-config', 'email-config', 'email-template', 'license', 'migration', 'deploy-bundle', 'environment-artifact', 'app-install', 'provisioning', 'tenant'] }, - { section: 'Services & Infrastructure', pages: ['core-services', 'http-server', 'cache', 'message-queue', 'object-storage', 'search-engine', 'worker', 'job', 'notification', 'translation', 'metadata-loader', 'metadata-persistence'] }, + // `metadata-loader` removed at #4696 — that file lives in `kernel/`, and the + // two schemas `system/` re-exports from it are documented on + // `system/metadata-persistence`, the file that re-exports them. + { section: 'Services & Infrastructure', pages: ['core-services', 'http-server', 'cache', 'message-queue', 'object-storage', 'search-engine', 'worker', 'job', 'notification', 'translation', 'metadata-persistence'] }, { section: 'Observability', pages: ['logging', 'metrics', 'tracing', 'audit'] }, { section: 'Security & Compliance', pages: ['encryption', 'security-context', 'incident-response', 'supplier-security', 'disaster-recovery', 'change-management', 'training'] }, { section: 'Content & Collaboration', pages: ['doc', 'book', 'collaboration'] }, @@ -461,7 +492,11 @@ const SECTION_GROUPS: Record // leaving the names here would have been silently harmless — which is why // they are removed deliberately instead. { section: 'Interaction & Layout', pages: ['responsive', 'theme'] }, - { section: 'Platform', pages: ['i18n', 'notification', 'sharing', 'http'] }, + // `http` removed at #4696 — `ui/` has no `http.zod.ts`; `HttpMethod` and + // `HttpRequest` reach this entry point through `ui/view.zod.ts`, whose own + // file comment already says so ("Migrated to shared/http.zod.ts. + // Re-exported here…"), and are documented on `ui/view` now. + { section: 'Platform', pages: ['i18n', 'notification', 'sharing'] }, ], }; @@ -548,7 +583,12 @@ Object.keys(CATEGORIES).forEach(category => { const schemaName = file.replace('.json', ''); const schemaPath = path.join(categorySchemaDir, file); const content = JSON.parse(fs.readFileSync(schemaPath, 'utf-8')); - const zodFile = schemaZodFileMap.get(schemaName) || 'misc'; + // Category-scoped: the page is owned by the file in THIS category that puts + // the name on its export surface — declaration or re-export. `misc` stays + // the catch-all for a published schema no `.zod.ts` here accounts for + // (`security/*` declares two in plain `.ts` files), and it is honest about + // it: `sourcePathFor` finds no file, so the page prints no "Source:" line. + const zodFile = schemaIndex.pageFor(category, schemaName) || 'misc'; if (!zodFileSchemas.has(zodFile)) { zodFileSchemas.set(zodFile, []); diff --git a/packages/spec/scripts/lib/schema-index.ts b/packages/spec/scripts/lib/schema-index.ts new file mode 100644 index 0000000000..fbc4df04b8 --- /dev/null +++ b/packages/spec/scripts/lib/schema-index.ts @@ -0,0 +1,230 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Schema name -> owning reference page, keyed by `${category}/${name}`. + * + * `build-docs.ts` used to key both of its index maps by the BARE schema name, + * globally. Two consequences, one hidden inside the other: + * + * 1. **Same name, two categories, last writer wins.** The winning side's page + * SLUG was then used to place the losing side's schema, so a schema landed + * on a page named after a file that does not exist in its own category + * (#4696, found while closing #4684's `RateLimitConfig` dual source). + * 2. **A re-export is invisible.** The scan matched `export const X` only, so a + * name that reaches `@objectstack/spec/` through + * `export { XSchema } from '../other/y.zod'` — or a bare + * `export { XSchema }` of an imported binding — had NO entry for its own + * category at all, and fell through to (1) by construction. That is where + * 25 of the 26 mis-placed schemas on `main` came from, not from name + * collisions. + * + * The def key `build-schemas.ts` publishes is already `${category}/${name}` + * (that is what `json-schema//.json` means), so the docs index + * converges on the same key rather than inventing a second identity for the + * same schema. + * + * ## What counts as "this category exports it" + * + * Exactly what `build-schemas.ts` sees, which is the category's runtime export + * surface — so both a declaration and a value re-export count. Type-only + * exports (`export type { X }`, `export { type X }`) do NOT: they publish no + * `z.ZodType`, so they never produce a JSON Schema and must not claim a page. + * + * ## Which file OWNS the page + * + * A declaration beats a re-export: `api/realtime-shared.zod.ts` declares + * `PresenceStatus` while `realtime.zod.ts` and `websocket.zod.ts` re-export it, + * and the page belongs to the declaration. Everything else is a real ambiguity + * and is reported as a conflict instead of being resolved by iteration order — + * see `SchemaNameConflict`. + */ + +/** How a `.zod.ts` file puts a name on its category's export surface. */ +export type SchemaExportKind = 'declaration' | 're-export'; + +/** One `.zod.ts` file, as the scanner hands it over. */ +export interface ZodFileInput { + /** `src/` sub-directory the file lives in (`data`, `ui`, `shared`, …). */ + category: string; + /** Page slug the file takes (`driver/postgres.zod.ts` -> `driver-postgres`). */ + slug: string; + /** Path relative to `src//` — used verbatim in error messages. */ + rel: string; + /** File contents. */ + source: string; +} + +/** + * A name whose owning page inside ONE category cannot be decided. + * + * Two shapes qualify, and neither may be resolved by overwrite: + * - two files DECLARE the name (the #4684 shape, moved inside one category); + * - no file declares it and two or more re-export it, so there is no + * declaration to break the tie. + */ +export interface SchemaNameConflict { + category: string; + name: string; + /** Candidate files, `rel` paths, sorted — each with how it exports the name. */ + sites: Array<{ rel: string; kind: SchemaExportKind }>; +} + +export interface SchemaIndex { + /** Page slug owning `/`, or `undefined` when nothing exports it. */ + pageFor(category: string, name: string): string | undefined; + /** Categories whose own `.zod.ts` files DECLARE `name` (re-exports excluded), sorted. */ + declaringCategories(name: string): string[]; + /** Every unresolvable name, sorted — a build-stopping condition, never a warning. */ + readonly conflicts: readonly SchemaNameConflict[]; +} + +/** + * Value exports a `.zod.ts` file names, with how each one got there. + * + * Anchored to the start of a line, because `export` is only legal at module + * top level and the alternative is matching the code samples inside TSDoc + * comments — four of which (`leadSeed`, `SETUP_APP`, `nightlySync`, + * `reportForm`) were entries in the old bare-name index, describing exports + * that do not exist. + */ +export function exportedValueNames(source: string): Array<{ raw: string; kind: SchemaExportKind }> { + const out: Array<{ raw: string; kind: SchemaExportKind }> = []; + + const declaration = /^export const (\w+)\s*[:=]/gm; + let m: RegExpExecArray | null; + while ((m = declaration.exec(source)) !== null) out.push({ raw: m[1], kind: 'declaration' }); + + // `export { A, B as C, type D } from '…';` and the from-less binding form. + // `export type { … }` is skipped whole: no runtime value, so no JSON Schema. + const clause = /^export\s+(type\s+)?\{([^}]*)\}/gm; + while ((m = clause.exec(source)) !== null) { + if (m[1]) continue; + for (const raw of m[2].split(',')) { + const specifier = raw.trim(); + if (!specifier) continue; + const parsed = /^(type\s+)?(\w+)(?:\s+as\s+(\w+))?$/.exec(specifier); + // An export clause this module cannot read would silently shrink the + // index and hand the page back to the cross-category fallback — the very + // failure mode #4696 is about. Fail instead of under-reporting. + if (!parsed) { + throw new Error( + `schema-index: cannot parse export specifier "${specifier}" — extend exportedValueNames() rather than letting the name drop out of the index`, + ); + } + if (parsed[1]) continue; + out.push({ raw: parsed[3] || parsed[2], kind: 're-export' }); + } + } + + return out; +} + +/** + * Build the category-keyed index. `toSchemaName` is injected so the suffix rule + * stays in `lib/schema-name.ts` alone (#4592) — this module must not grow a + * second copy of it. + */ +export function buildSchemaIndex( + files: readonly ZodFileInput[], + toSchemaName: (exportKey: string) => string, +): SchemaIndex { + // `${category}/${name}` -> slug -> { rel, kinds } + const sites = new Map }>>(); + const declaringByName = new Map>(); + + for (const file of files) { + for (const { raw, kind } of exportedValueNames(file.source)) { + const name = toSchemaName(raw); + const key = `${file.category}/${name}`; + let bySlug = sites.get(key); + if (!bySlug) sites.set(key, (bySlug = new Map())); + let site = bySlug.get(file.slug); + if (!site) bySlug.set(file.slug, (site = { rel: file.rel, kinds: new Set() })); + site.kinds.add(kind); + + if (kind === 'declaration') { + let cats = declaringByName.get(name); + if (!cats) declaringByName.set(name, (cats = new Set())); + cats.add(file.category); + } + } + } + + const owner = new Map(); + const conflicts: SchemaNameConflict[] = []; + + for (const [key, bySlug] of sites) { + const declaring = [...bySlug].filter(([, s]) => s.kinds.has('declaration')); + // One declaration is the answer however many files re-export it. + if (declaring.length === 1) { owner.set(key, declaring[0][0]); continue; } + if (declaring.length === 0 && bySlug.size === 1) { owner.set(key, [...bySlug.keys()][0]); continue; } + + const sep = key.indexOf('/'); + conflicts.push({ + category: key.slice(0, sep), + name: key.slice(sep + 1), + sites: [...bySlug] + .map(([, s]) => ({ + rel: s.rel, + kind: (s.kinds.has('declaration') ? 'declaration' : 're-export') as SchemaExportKind, + })) + .sort((a, b) => a.rel.localeCompare(b.rel)), + }); + } + + conflicts.sort((a, b) => `${a.category}/${a.name}`.localeCompare(`${b.category}/${b.name}`)); + + return { + pageFor: (category, name) => owner.get(`${category}/${name}`), + declaringCategories: (name) => [...(declaringByName.get(name) ?? [])].sort(), + conflicts, + }; +} + +/** + * The page a reference from `fromCategory` should link `name` to. + * + * Own category first: the reader is on a page whose import example says + * `from '@objectstack/spec/'`, and that entry point really does + * export the name, so sending them to another category's page would hand them a + * different import path than the one they came from — and would leave the + * category's own section for that name unreachable, which is the ghost-page + * pathology one level down. + * + * Otherwise the DECLARING category, and only when exactly one declares it. Two + * declarations mean the name alone does not identify a schema (the #4684 + * shape), so no link is emitted at all — a `$ref` renders as plain text rather + * than as a confident link to the wrong schema. + */ +export function resolveSchemaPage( + index: SchemaIndex, + fromCategory: string, + name: string, +): { category: string; slug: string } | null { + const own = index.pageFor(fromCategory, name); + if (own) return { category: fromCategory, slug: own }; + + const declaring = index.declaringCategories(name); + if (declaring.length !== 1) return null; + + const slug = index.pageFor(declaring[0], name); + return slug ? { category: declaring[0], slug } : null; +} + +/** The build-stopping message for `SchemaIndex.conflicts`. */ +export function formatConflicts(conflicts: readonly SchemaNameConflict[]): string { + const lines = conflicts.map((c) => { + const sites = c.sites.map((s) => ` - ${c.category}/${s.rel} (${s.kind})`).join('\n'); + return ` ${c.category}/${c.name}\n${sites}`; + }); + return ( + `${conflicts.length} schema name(s) have no single owning page inside their category:\n\n` + + `${lines.join('\n')}\n\n` + + `The docs index is keyed by \`/\` (#4696), so one of these files has to win —\n` + + `and picking one by directory-walk order is how a schema ends up documented on a page named\n` + + `after a file that does not contain it. Fix it at the source, not here:\n` + + ` - two DECLARATIONS: rename one (that is what #4684 did for RateLimitConfig), or delete the\n` + + ` duplicate and re-export the survivor;\n` + + ` - two RE-EXPORTS and no declaration: keep the one the category should own.\n` + ); +} diff --git a/packages/spec/scripts/schema-index.test.ts b/packages/spec/scripts/schema-index.test.ts new file mode 100644 index 0000000000..66bfe1f332 --- /dev/null +++ b/packages/spec/scripts/schema-index.test.ts @@ -0,0 +1,240 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Pins the docs index's KEY STRUCTURE (#4696). + * + * `build-docs.ts` is a top-level script with side effects, so the only way to + * assert on its page placement used to be to run the whole generator and read + * the emitted `.mdx` — which is exactly how a bare-name global index survived + * long enough to write 26 schemas onto pages named after files that do not + * contain them. The index is extracted here for the same reason `format-type` + * (#4912) and `schema-name` (#4592) were. + * + * Three facts carry the fix, and each is a separate test below: + * + * 1. the same name in two categories is two schemas, each on its own page; + * 2. a name that reaches a category by RE-EXPORT belongs to the re-exporting + * file's page, not to the declaring file's slug borrowed into a category + * where no such file exists; + * 3. an undecidable name inside one category is an ERROR, never an overwrite. + */ +import { describe, expect, it } from 'vitest'; + +import { + buildSchemaIndex, + exportedValueNames, + formatConflicts, + resolveSchemaPage, + type ZodFileInput, +} from './lib/schema-index'; +import { schemaNameFromExportKey } from './lib/schema-name'; + +const file = (category: string, rel: string, source: string): ZodFileInput => ({ + category, + rel, + slug: rel.replace(/\.zod\.ts$/, '').replace(/\//g, '-'), + source, +}); + +const index = (...files: ZodFileInput[]) => buildSchemaIndex(files, schemaNameFromExportKey); + +describe('exportedValueNames', () => { + it('reads a declaration, in both `=` and `: T =` spellings', () => { + expect(exportedValueNames([ + 'export const WidgetSchema = z.object({});', + 'export const GadgetSchema: z.ZodType = lazySchema(() => z.object({}));', + ].join('\n'))).toEqual([ + { raw: 'WidgetSchema', kind: 'declaration' }, + { raw: 'GadgetSchema', kind: 'declaration' }, + ]); + }); + + it('reads a re-export, with and without a `from` clause', () => { + expect(exportedValueNames([ + "export { RetryPolicySchema } from '../shared/retry-policy.zod';", + 'export { HttpMethodSchema, HttpRequestSchema };', + ].join('\n'))).toEqual([ + { raw: 'RetryPolicySchema', kind: 're-export' }, + { raw: 'HttpMethodSchema', kind: 're-export' }, + { raw: 'HttpRequestSchema', kind: 're-export' }, + ]); + }); + + it('takes the ALIAS of a renamed re-export — that is the published name', () => { + expect(exportedValueNames("export { InnerSchema as PublicSchema } from './inner.zod';")) + .toEqual([{ raw: 'PublicSchema', kind: 're-export' }]); + }); + + it('skips type-only exports in both spellings — they publish no z.ZodType', () => { + // `build-schemas.ts` emits a JSON Schema only for a runtime ZodType value, + // so a type-only export must not claim a page. `system/job.zod.ts` mixes + // both in one clause, which is why the inline modifier is handled too. + expect(exportedValueNames([ + "export type { RetryPolicy } from '../shared/retry-policy.zod';", + "export { RetryPolicySchema, type RetryPolicy, type RetryPolicyParsed } from '../shared/retry-policy.zod';", + ].join('\n'))).toEqual([{ raw: 'RetryPolicySchema', kind: 're-export' }]); + }); + + it('ignores exports that only appear inside a TSDoc code sample', () => { + // The old scan was unanchored and indexed four of these (`leadSeed`, + // `SETUP_APP`, `nightlySync`, `reportForm`) as real exports. + expect(exportedValueNames([ + '/**', + ' * ```ts', + ' * export const leadSeed = defineSeed({});', + " * export { GhostSchema } from './ghost.zod';", + ' * ```', + ' */', + 'export const SeedSchema = z.object({});', + ].join('\n'))).toEqual([{ raw: 'SeedSchema', kind: 'declaration' }]); + }); + + it('throws rather than dropping an export clause it cannot read', () => { + // Under-reporting here silently hands the page back to the cross-category + // fallback — the #4696 failure mode itself. + expect(() => exportedValueNames('export { A B } from "./x";')).toThrow(/cannot parse export specifier/); + }); +}); + +describe('buildSchemaIndex — cross-category same name', () => { + // The live specimen on `main`: `ServiceStatus` is an enum declared in + // `api/discovery.zod.ts` AND an object declared in + // `system/core-services.zod.ts`. The bare-name index let `system` (walked + // later) win, and then placed the API schema on `api/core-services.mdx` — + // a page with no `api/core-services.zod.ts` behind it. + const idx = index( + file('api', 'discovery.zod.ts', 'export const ServiceStatus = z.enum([]);'), + file('system', 'core-services.zod.ts', 'export const ServiceStatusSchema = z.object({});'), + ); + + it('gives each declaration its own page, in its own category', () => { + expect(idx.pageFor('api', 'ServiceStatus')).toBe('discovery'); + expect(idx.pageFor('system', 'ServiceStatus')).toBe('core-services'); + }); + + it('is not a conflict — separate categories are separate published schemas', () => { + expect(idx.conflicts).toEqual([]); + }); + + it('never borrows the other category\'s slug', () => { + // The regression this whole issue is about: `api` has no `core-services` + // page, so nothing may resolve to one. + expect(idx.pageFor('api', 'ServiceStatus')).not.toBe('core-services'); + }); +}); + +describe('buildSchemaIndex — re-exports', () => { + it('places a re-exported name on the RE-EXPORTING file\'s page', () => { + // `automation/control-flow.zod.ts` re-exports `RetryPolicySchema` from + // `shared/`. The old index had no `automation/RetryPolicy` entry at all and + // fell back to the declaring file's slug, inventing + // `automation/retry-policy.mdx`. + const idx = index( + file('shared', 'retry-policy.zod.ts', 'export const RetryPolicySchema = z.object({});'), + file('automation', 'control-flow.zod.ts', "export { RetryPolicySchema } from '../shared/retry-policy.zod';"), + ); + expect(idx.pageFor('shared', 'RetryPolicy')).toBe('retry-policy'); + expect(idx.pageFor('automation', 'RetryPolicy')).toBe('control-flow'); + }); + + it('lets the DECLARATION win over any number of same-category re-exports', () => { + // Live shape: `api/realtime-shared.zod.ts` declares `PresenceStatus`, and + // both `realtime.zod.ts` and `websocket.zod.ts` re-export it. One owner, + // no ambiguity, no error. + const idx = index( + file('api', 'realtime-shared.zod.ts', 'export const PresenceStatusSchema = z.enum([]);'), + file('api', 'realtime.zod.ts', "export { PresenceStatusSchema } from './realtime-shared.zod';"), + file('api', 'websocket.zod.ts', "export { PresenceStatusSchema } from './realtime-shared.zod';"), + ); + expect(idx.pageFor('api', 'PresenceStatus')).toBe('realtime-shared'); + expect(idx.conflicts).toEqual([]); + }); + + it('reports nothing for a name no file in the category exports', () => { + // The caller then uses the `misc` catch-all, whose page prints no "Source:". + const idx = index(file('data', 'object.zod.ts', 'export const ObjectSchema = z.object({});')); + expect(idx.pageFor('security', 'TenancyPosture')).toBeUndefined(); + }); +}); + +describe('buildSchemaIndex — conflicts are errors, never overwrites', () => { + it('fails when two files in one category DECLARE the same name', () => { + const idx = index( + file('integration', 'connector.zod.ts', 'export const RateLimitConfigSchema = z.object({});'), + file('integration', 'http.zod.ts', 'export const RateLimitConfigSchema = z.object({});'), + ); + expect(idx.conflicts).toEqual([ + { + category: 'integration', + name: 'RateLimitConfig', + sites: [ + { rel: 'connector.zod.ts', kind: 'declaration' }, + { rel: 'http.zod.ts', kind: 'declaration' }, + ], + }, + ]); + // And no owner is invented for it. + expect(idx.pageFor('integration', 'RateLimitConfig')).toBeUndefined(); + }); + + it('fails when two files re-export the same name and none declares it', () => { + // Nothing breaks the tie, so the generator must not pick one. + const idx = index( + file('shared', 'http.zod.ts', 'export const HttpMethodSchema = z.enum([]);'), + file('ui', 'view.zod.ts', 'export { HttpMethodSchema };'), + file('ui', 'page.zod.ts', "export { HttpMethodSchema } from '../shared/http.zod';"), + ); + expect(idx.conflicts.map((c) => `${c.category}/${c.name}`)).toEqual(['ui/HttpMethod']); + expect(idx.pageFor('ui', 'HttpMethod')).toBeUndefined(); + // The unrelated declaring category is untouched. + expect(idx.pageFor('shared', 'HttpMethod')).toBe('http'); + }); + + it('names both files and the fix in the error message', () => { + const idx = index( + file('integration', 'connector.zod.ts', 'export const RateLimitConfigSchema = z.object({});'), + file('integration', 'http.zod.ts', 'export const RateLimitConfigSchema = z.object({});'), + ); + const message = formatConflicts(idx.conflicts); + expect(message).toContain('integration/RateLimitConfig'); + expect(message).toContain('integration/connector.zod.ts (declaration)'); + expect(message).toContain('integration/http.zod.ts (declaration)'); + expect(message).toContain('#4696'); + }); +}); + +describe('resolveSchemaPage', () => { + const idx = index( + file('shared', 'http.zod.ts', 'export const HttpMethodSchema = z.enum([]);'), + file('api', 'router.zod.ts', 'export { HttpMethodSchema };'), + file('api', 'discovery.zod.ts', 'export const ServiceStatus = z.enum([]);'), + file('system', 'core-services.zod.ts', 'export const ServiceStatusSchema = z.object({});'), + file('data', 'object.zod.ts', 'export const ObjectSchema = z.object({});'), + ); + + it('links within the reader\'s own category when it exports the name', () => { + // `api/router.mdx` documents `HttpMethod` with an + // `import … from '@objectstack/spec/api'` example; sending an api reader to + // `shared/` would hand them a different import path than the page they are + // on, and would leave api's own section for the name unreachable. + expect(resolveSchemaPage(idx, 'api', 'HttpMethod')).toEqual({ category: 'api', slug: 'router' }); + }); + + it('falls back to the single DECLARING category otherwise', () => { + expect(resolveSchemaPage(idx, 'data', 'HttpMethod')).toEqual({ category: 'shared', slug: 'http' }); + }); + + it('emits NO link when the bare name does not identify one schema', () => { + // Two declarations, two different schemas. The old global lookup answered + // with whichever the directory walk reached last — a confident link to the + // wrong schema is worse than plain text. + expect(resolveSchemaPage(idx, 'data', 'ServiceStatus')).toBeNull(); + // …but each category that owns one still links to its own. + expect(resolveSchemaPage(idx, 'api', 'ServiceStatus')).toEqual({ category: 'api', slug: 'discovery' }); + expect(resolveSchemaPage(idx, 'system', 'ServiceStatus')).toEqual({ category: 'system', slug: 'core-services' }); + }); + + it('emits no link for a name nothing exports', () => { + expect(resolveSchemaPage(idx, 'data', 'NoSuchSchema')).toBeNull(); + }); +});