From 0f320f8a32a9bea3454951b439ddd851b21b88bc Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sat, 30 May 2026 21:04:29 +0800 Subject: [PATCH] fix: resolve build and test errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three stale tests/tooling didn't match intentional source behavior, breaking `pnpm build` / `pnpm test`. No shipped runtime behavior is changed. 1. MDX docs build (packages/spec/scripts/build-docs.ts) — @objectstack/docs#build escapeMdxDescription used two separate regex passes for `{...}` and `<...>`. On nested tokens like `{}` it double-wrapped into `` `{``}` ``, whose inner backticks closed the inline-code span early and leaked `` as raw JSX ("Expected a closing tag for ``"). Replaced with a single backtick-aware pass that skips fragments already inside code spans, and regenerated the affected .mdx. 2. objectql registry test (packages/objectql/src/protocol-meta.test.ts) "loads from a complete artifact" asserted strict toEqual against objDef, but object schemas pass through registerObject -> applyProtection which stamps internal envelope markers (ADR-0010 §3.7). Switched to toMatchObject to assert the author-supplied shape is preserved rather than demanding equality against intentional system fields. 3. CLI serve-defaults test (packages/cli/test/serve-defaults.test.ts) The test pinned ALWAYS_ON_CAPABILITIES to exactly six items, but the shipped fail-closed allowlist now also includes `sharing`. The six foundational capabilities are still required to lead the slate in order, so the test now pins that prefix (slice(0,6)) plus a no-duplicates invariant, letting the list grow without churning the assertion. Build: 63/63 tasks pass. Test: 123/123 tasks pass. --- content/docs/references/api/contract.mdx | 2 +- content/docs/references/api/metadata.mdx | 4 +- content/docs/references/data/datasource.mdx | 38 ++++++++- .../docs/references/data/external-catalog.mdx | 82 +++++++++++++++++++ content/docs/references/data/index.mdx | 1 + content/docs/references/data/meta.json | 1 + content/docs/references/data/object.mdx | 22 ++++- content/docs/references/data/query.mdx | 2 +- .../references/kernel/metadata-plugin.mdx | 7 +- content/docs/references/ui/app.mdx | 2 +- packages/cli/test/serve-defaults.test.ts | 15 +++- packages/objectql/src/protocol-meta.test.ts | 7 +- packages/spec/scripts/build-docs.ts | 40 +++++++-- 13 files changed, 200 insertions(+), 23 deletions(-) create mode 100644 content/docs/references/data/external-catalog.mdx diff --git a/content/docs/references/api/contract.mdx b/content/docs/references/api/contract.mdx index 38dd30e2c..3b48a7d11 100644 --- a/content/docs/references/api/contract.mdx +++ b/content/docs/references/api/contract.mdx @@ -150,7 +150,7 @@ const result = ApiError.parse(data); | **cursor** | `Record` | optional | Cursor for keyset pagination | | **joins** | `Object[]` | optional | Explicit Table Joins | | **aggregations** | `Object[]` | optional | Aggregation functions | -| **groupBy** | `string \| Object[]` | optional | GROUP BY targets (strings or ``{field, dateGranularity?}`` objects for date bucketing) | +| **groupBy** | `string \| Object[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) | | **having** | `[__schema2](./__schema2)` | optional | HAVING clause for aggregation filtering | | **windowFunctions** | `Object[]` | optional | Window functions with OVER clause | | **distinct** | `boolean` | optional | SELECT DISTINCT flag | diff --git a/content/docs/references/api/metadata.mdx b/content/docs/references/api/metadata.mdx index b8f79b47d..6bc4565e0 100644 --- a/content/docs/references/api/metadata.mdx +++ b/content/docs/references/api/metadata.mdx @@ -314,7 +314,7 @@ Metadata query with filtering, sorting, and pagination | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **types** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types | +| **types** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types | | **namespaces** | `string[]` | optional | Filter by namespaces | | **packageId** | `string` | optional | Filter by owning package | | **search** | `string` | optional | Full-text search query | @@ -349,7 +349,7 @@ Metadata query with filtering, sorting, and pagination | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type | +| **type** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type | | **name** | `string` | ✅ | Item name (snake_case) | | **data** | `Record` | ✅ | Metadata payload | | **namespace** | `string` | optional | Optional namespace | diff --git a/content/docs/references/data/datasource.mdx b/content/docs/references/data/datasource.mdx index 6f22abafa..94bc99288 100644 --- a/content/docs/references/data/datasource.mdx +++ b/content/docs/references/data/datasource.mdx @@ -16,8 +16,8 @@ Can be a built-in driver or a plugin-contributed driver (e.g., "com.vendor.snowf ## TypeScript Usage ```typescript -import { Datasource, DatasourceCapabilities, DriverDefinition, DriverType } from '@objectstack/spec/data'; -import type { Datasource, DatasourceCapabilities, DriverDefinition, DriverType } from '@objectstack/spec/data'; +import { Datasource, DatasourceCapabilities, DriverDefinition, DriverType, ExternalDatasourceSettings, ModeSchema } from '@objectstack/spec/data'; +import type { Datasource, DatasourceCapabilities, DriverDefinition, DriverType, ExternalDatasourceSettings, Mode } from '@objectstack/spec/data'; // Validate data const result = Datasource.parse(data); @@ -43,6 +43,8 @@ const result = Datasource.parse(data); | **retryPolicy** | `Object` | optional | Connection retry policy for transient failures | | **description** | `string` | optional | Internal description | | **active** | `boolean` | ✅ | Is datasource enabled | +| **schemaMode** | `Enum<'managed' \| 'external' \| 'validate-only'>` | ✅ | Schema ownership mode | +| **external** | `Object` | optional | External datasource federation settings (schemaMode != "managed") | --- @@ -87,3 +89,35 @@ const result = Datasource.parse(data); --- +## ExternalDatasourceSettings + +External datasource federation settings (schemaMode != "managed") + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **label** | `string` | optional | Display label, e.g. "Snowflake — ANALYTICS / PROD" | +| **allowedSchemas** | `string[]` | optional | Whitelist of remote schemas/databases that may be exposed. | +| **allowWrites** | `boolean` | ✅ | Global write gate. Individual objects must also opt in via object.external.writable. | +| **validation** | `Object` | ✅ | Boot/drift validation policy | +| **credentialsRef** | `string` | optional | Reference into the secrets store; never inline credentials. | +| **queryTimeoutMs** | `number` | ✅ | Hard cap on per-query execution time. | +| **requirePermission** | `string` | optional | Optional convenience: gate the entire datasource behind a single role. | + + +--- + +## ModeSchema + +Schema ownership mode + +### Allowed Values + +* `managed` +* `external` +* `validate-only` + + +--- + diff --git a/content/docs/references/data/external-catalog.mdx b/content/docs/references/data/external-catalog.mdx new file mode 100644 index 000000000..810b8a267 --- /dev/null +++ b/content/docs/references/data/external-catalog.mdx @@ -0,0 +1,82 @@ +--- +title: External Catalog +description: External Catalog protocol schemas +--- + +{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs go in content/docs/guides/. */} + +ExternalCatalog — cached remote-schema snapshot for a federated datasource + +(ADR-0015 §4.3). + +Introspecting a mature warehouse on every boot is expensive, so the + +`IExternalDatasourceService.refreshCatalog` persists a snapshot of the + +remote tables/columns as an `external_catalog` metadata record. The + +boot-validation gate (Gate 2) and Studio's schema browser read from it; + +drift is detected by diffing a fresh introspection against the snapshot. + + +**Source:** `packages/spec/src/data/external-catalog.zod.ts` + + +## TypeScript Usage + +```typescript +import { ExternalCatalog, ExternalColumn, ExternalTable } from '@objectstack/spec/data'; +import type { ExternalCatalog, ExternalColumn, ExternalTable } from '@objectstack/spec/data'; + +// Validate data +const result = ExternalCatalog.parse(data); +``` + +--- + +## ExternalCatalog + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Catalog id, conventionally `_catalog`. | +| **datasource** | `string` | ✅ | Datasource.name this catalog snapshots. | +| **snapshotAt** | `string` | ✅ | When the snapshot was taken (ISO 8601). | +| **dialect** | `string` | optional | Remote SQL dialect, when known. | +| **tables** | `Object[]` | ✅ | Snapshotted remote tables. | + + +--- + +## ExternalColumn + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **name** | `string` | ✅ | Remote column name | +| **sqlType** | `string` | ✅ | Raw remote SQL type (e.g. "numeric(10,2)") | +| **nullable** | `boolean` | ✅ | Whether the remote column is nullable | +| **primaryKey** | `boolean` | ✅ | Part of the remote primary key | +| **suggestedFieldType** | `string` | optional | ObjectStack field type suggested by the type-compat matrix | + + +--- + +## ExternalTable + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **remoteSchema** | `string` | optional | Remote schema/database qualifier | +| **remoteName** | `string` | ✅ | Remote table/view name | +| **columns** | `Object[]` | ✅ | Remote columns | +| **indexes** | `Object[]` | optional | Remote indexes, when introspectable | +| **rowCountEstimate** | `number` | optional | Approximate row count | + + +--- + diff --git a/content/docs/references/data/index.mdx b/content/docs/references/data/index.mdx index e85cd86a2..f9ffc2bc9 100644 --- a/content/docs/references/data/index.mdx +++ b/content/docs/references/data/index.mdx @@ -15,6 +15,7 @@ This section contains all protocol schemas for the data layer of ObjectStack. + diff --git a/content/docs/references/data/meta.json b/content/docs/references/data/meta.json index 4605ea751..617e642ac 100644 --- a/content/docs/references/data/meta.json +++ b/content/docs/references/data/meta.json @@ -10,6 +10,7 @@ "driver", "driver-nosql", "driver-sql", + "external-catalog", "external-lookup", "feed", "field", diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index a41228db2..059d0bfda 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -14,8 +14,8 @@ API Operations Enum ## TypeScript Usage ```typescript -import { ApiMethod, CDCConfig, Index, ObjectCapabilities, ObjectOwnershipEnum, PartitioningConfig, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data'; -import type { ApiMethod, CDCConfig, Index, ObjectCapabilities, ObjectOwnershipEnum, PartitioningConfig, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data'; +import { ApiMethod, CDCConfig, Index, ObjectCapabilities, ObjectExternalBinding, ObjectOwnershipEnum, PartitioningConfig, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data'; +import type { ApiMethod, CDCConfig, Index, ObjectCapabilities, ObjectExternalBinding, ObjectOwnershipEnum, PartitioningConfig, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data'; // Validate data const result = ApiMethod.parse(data); @@ -91,6 +91,24 @@ const result = ApiMethod.parse(data); | **clone** | `boolean` | ✅ | Allow record deep cloning | +--- + +## ObjectExternalBinding + +External datasource binding (ADR-0015) + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **remoteName** | `string` | optional | Remote table/view name. Defaults to object.name. | +| **remoteSchema** | `string` | optional | Remote schema/database qualifier. | +| **writable** | `boolean` | ✅ | Per-object write opt-in (also requires datasource.external.allowWrites). | +| **columnMap** | `Record` | optional | Remote column name → local field name. | +| **introspectedAt** | `string` | optional | Set by `os datasource introspect`; informational. | +| **ignoreColumns** | `string[]` | optional | Remote columns to skip during validation (dev convenience). | + + --- ## ObjectOwnershipEnum diff --git a/content/docs/references/data/query.mdx b/content/docs/references/data/query.mdx index bc6e43466..2ae0511aa 100644 --- a/content/docs/references/data/query.mdx +++ b/content/docs/references/data/query.mdx @@ -198,7 +198,7 @@ Type: `string` | **cursor** | `Record` | optional | Cursor for keyset pagination | | **joins** | `Object[]` | optional | Explicit Table Joins | | **aggregations** | `Object[]` | optional | Aggregation functions | -| **groupBy** | `string \| Object[]` | optional | GROUP BY targets (strings or ``{field, dateGranularity?}`` objects for date bucketing) | +| **groupBy** | `string \| Object[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) | | **having** | `[__schema1](./__schema1)` | optional | HAVING clause for aggregation filtering | | **windowFunctions** | `Object[]` | optional | Window functions with OVER clause | | **distinct** | `boolean` | optional | SELECT DISTINCT flag | diff --git a/content/docs/references/kernel/metadata-plugin.mdx b/content/docs/references/kernel/metadata-plugin.mdx index d3ca215a4..73edf30e5 100644 --- a/content/docs/references/kernel/metadata-plugin.mdx +++ b/content/docs/references/kernel/metadata-plugin.mdx @@ -130,7 +130,7 @@ const result = MetadataBulkRegisterRequest.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **event** | `Enum<'metadata.registered' \| 'metadata.updated' \| 'metadata.unregistered' \| 'metadata.validated' \| 'metadata.deployed' \| 'metadata.overlay.applied' \| 'metadata.overlay.removed' \| 'metadata.imported' \| 'metadata.exported'>` | ✅ | Event type | -| **metadataType** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type | +| **metadataType** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type | | **name** | `string` | ✅ | Metadata item name | | **namespace** | `string` | optional | Namespace | | **packageId** | `string` | optional | Owning package ID | @@ -183,7 +183,7 @@ const result = MetadataBulkRegisterRequest.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **types** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types | +| **types** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types | | **namespaces** | `string[]` | optional | Filter by namespaces | | **packageId** | `string` | optional | Filter by owning package | | **search** | `string` | optional | Full-text search query | @@ -232,6 +232,7 @@ const result = MetadataBulkRegisterRequest.parse(data); * `approval` * `job` * `datasource` +* `external_catalog` * `translation` * `router` * `function` @@ -253,7 +254,7 @@ const result = MetadataBulkRegisterRequest.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type identifier | +| **type** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type identifier | | **label** | `string` | ✅ | Display label for the metadata type | | **description** | `string` | optional | Description of the metadata type | | **filePatterns** | `string[]` | ✅ | Glob patterns to discover files of this type | diff --git a/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx index 339c8e378..c84f1de3b 100644 --- a/content/docs/references/ui/app.mdx +++ b/content/docs/references/ui/app.mdx @@ -62,7 +62,7 @@ const result = AppBranding.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **id** | `string` | ✅ | Selector id; selected value is exposed as the nav template var `{``}` | +| **id** | `string` | ✅ | Selector id; selected value is exposed as the nav template var `{}` | | **label** | `string` | ✅ | Dropdown label | | **icon** | `string` | optional | Icon name | | **optionsSource** | `Object` | ✅ | Option data source | diff --git a/packages/cli/test/serve-defaults.test.ts b/packages/cli/test/serve-defaults.test.ts index efc11f77e..98e695a3b 100644 --- a/packages/cli/test/serve-defaults.test.ts +++ b/packages/cli/test/serve-defaults.test.ts @@ -2,12 +2,23 @@ import { describe, expect, it } from 'vitest'; import Serve from '../src/commands/serve.js'; describe('serve: ALWAYS_ON_CAPABILITIES default slate', () => { - it('exposes the six foundational capabilities in stable order', () => { - expect(Serve.ALWAYS_ON_CAPABILITIES).toEqual([ + // ALWAYS_ON_CAPABILITIES is the fail-closed allowlist of platform services + // that are injected into every app's `requires` at runtime. The six + // foundational capabilities must always lead the slate in this precedence + // order; the list may grow beyond them (e.g. `sharing`) without churning + // this assertion, so we pin the prefix rather than the whole array. + it('leads with the six foundational capabilities in stable order', () => { + expect(Serve.ALWAYS_ON_CAPABILITIES.slice(0, 6)).toEqual([ 'queue', 'job', 'cache', 'settings', 'email', 'storage', ]); }); + it('contains no duplicates', () => { + expect(Serve.ALWAYS_ON_CAPABILITIES).toHaveLength( + new Set(Serve.ALWAYS_ON_CAPABILITIES).size, + ); + }); + it('is frozen so accidental mutation throws', () => { expect(Object.isFrozen(Serve.ALWAYS_ON_CAPABILITIES)).toBe(true); }); diff --git a/packages/objectql/src/protocol-meta.test.ts b/packages/objectql/src/protocol-meta.test.ts index 506903b01..55abd2862 100644 --- a/packages/objectql/src/protocol-meta.test.ts +++ b/packages/objectql/src/protocol-meta.test.ts @@ -878,7 +878,12 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { expect(result.loaded).toBe(2); expect(registry.getItem('app', 'test_app')).toEqual(sampleApp); - expect(registry.getItem('object', 'task')).toEqual(objDef); + // Object schemas pass through registerObject -> applyProtection (ADR-0010 §3.7), + // which stamps the internal `_packageId`/`_provenance` envelope markers used by + // listItems() filtering, the HTTP dispatcher and the runtime. Those are an + // intentional, system-wide concern — assert the author-supplied shape is preserved + // rather than demanding strict equality against them. + expect(registry.getItem('object', 'task')).toMatchObject(objDef); }); }); diff --git a/packages/spec/scripts/build-docs.ts b/packages/spec/scripts/build-docs.ts index d05a8a0b9..61e484d91 100644 --- a/packages/spec/scripts/build-docs.ts +++ b/packages/spec/scripts/build-docs.ts @@ -158,14 +158,38 @@ function generateMarkdown(schemaName: string, schema: any, category: string, zod // Add schema heading md += `## ${schemaName}\n\n`; - // Escape MDX-unsafe characters in description text. MDX parses `<` as JSX, - // so any raw `` / `<meta>` / etc. inside a Zod `.describe()` string - // breaks the docs build. Wrap inline tag-like fragments in backticks so they - // render as code rather than being parsed as JSX. - const escapeMdxDescription = (raw: string): string => - raw - .replace(/\{[^}]*\}/g, (m: string) => `\`${m}\``) - .replace(/<[^`>][^>]*>/g, (m: string) => `\`${m}\``); + // Escape MDX-unsafe characters in description text. MDX parses `{` as a JS + // expression and `<` as JSX, so any raw `{token}` / `<title>` inside a Zod + // `.describe()` string breaks the docs build. Wrap such fragments in inline + // code so they render literally. + // + // Single pass with backtick tracking: fragments already inside an inline-code + // span are left untouched. A naive two-pass replace double-wraps nested cases + // like `{<id>}` into `` `{`<id>`}` `` — the inner backticks close the span + // early and leak `<id>` as raw JSX (MDX: "Expected a closing tag for `<id>`"). + const escapeMdxDescription = (raw: string): string => { + let out = ''; + let inCode = false; + for (let i = 0; i < raw.length; i++) { + const ch = raw[i]; + if (ch === '`') { + inCode = !inCode; + out += ch; + continue; + } + if (!inCode && (ch === '{' || ch === '<')) { + const close = ch === '{' ? '}' : '>'; + const end = raw.indexOf(close, i + 1); + if (end !== -1) { + out += '`' + raw.slice(i, end + 1) + '`'; + i = end; + continue; + } + } + out += ch; + } + return out; + }; // Add description with better formatting if (mainDef.description) {