From 5b1d8463f672b80d21db130e4b7c15c9c3b32fcd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 15:25:19 +0000 Subject: [PATCH] =?UTF-8?q?fix(spec):=20=E5=8F=82=E8=80=83=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E9=A1=B6=E5=B1=82=E9=95=BF=E6=9E=9A=E4=B8=BE=E7=A7=BB?= =?UTF-8?q?=E5=85=A5=20Allowed=20Values,=E8=81=94=E5=90=88=E5=8F=98?= =?UTF-8?q?=E4=BD=93=E5=8D=B0=E6=95=B0=E9=87=8F=20(#6225,=20#6226)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #5340(PR #6211)压掉内联摘要里的长枚举后,`content/docs/references/**` 仍有 27 个 超过 400 字符的类型单元格。它们是两种机制,一次重生成一起收: - #6225:9 个「整格就是一个 Enum」的顶层词表。新增 `formatPropertyType`,是 `build-docs.ts` 里 `### Allowed Values` 项目符号分支的镜像,逐条件对齐 (`type === 'string'` 且 enum 是数组)。单元格印样本加数量,完整成员表印在表格 正下方 —— 信息没有离开页面。省略与搬迁来自同一个 `elideEnum` 调用,不会各说各话; 预算只在会搬迁的入口读取,`formatType` 本身未改,#5340 的「自己那一行不省略」原样成立。 - #6226:联合变体数上限(维护者裁决),超出印 `… +N more`,自报被省略的变体数。 阈值全部实测:枚举预算 40–176 给出完全相同的宽度结果,区间内只影响搬走多少份词表 (227 → 24),取 160;变体上限按 353 个联合的分布(2 个变体占 72.5%,累计到 4 个 是 92.6%)取 4,与 `INLINE_KEY_LIMIT` 同源。「标记必须挣回自己的位置」守卫共用, 实测拒绝 248 个候选省略里的 54 个(合计只省 328 字符),其中 7 个是整节搬迁。 全语料:>400 27 → 9,>900 4 → 1,p99 247 → 227,max 6092 → 1538,p95 145 不动。 25 份被搬迁的词表逐条核过完整成员表就在本页(25/25)。MDX 编译 216/216。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5 --- ...-cell-width-enum-relocation-variant-cap.md | 75 +++++ .../docs/references/ai/solution-blueprint.mdx | 54 +++- content/docs/references/api/contract.mdx | 266 +++++++++++++++++- content/docs/references/api/errors.mdx | 90 +++++- content/docs/references/api/events.mdx | 44 ++- content/docs/references/api/metadata.mdx | 31 +- content/docs/references/data/field.mdx | 54 +++- content/docs/references/data/validation.mdx | 4 +- content/docs/references/kernel/manifest.mdx | 2 +- .../references/kernel/metadata-plugin.mdx | 31 +- .../references/kernel/package-artifact.mdx | 20 +- .../docs/references/kernel/plugin-loading.mdx | 19 +- .../kernel/plugin-security-advanced.mdx | 21 +- .../docs/references/system/collaboration.mdx | 2 +- .../references/system/incident-response.mdx | 16 +- content/docs/references/system/metrics.mdx | 25 +- content/docs/references/system/migration.mdx | 4 +- .../references/system/settings-manifest.mdx | 24 +- content/docs/references/system/training.mdx | 15 +- content/docs/references/ui/action.mdx | 90 +++++- content/docs/references/ui/app.mdx | 6 +- content/docs/references/ui/bulk-action.mdx | 54 +++- content/docs/references/ui/chart.mdx | 50 +++- content/docs/references/ui/dashboard.mdx | 25 +- content/docs/references/ui/report.mdx | 25 +- content/docs/references/ui/view.mdx | 78 ++++- packages/spec/scripts/build-docs.ts | 23 +- packages/spec/scripts/format-type.test.ts | 251 ++++++++++++++++- packages/spec/scripts/lib/format-type.ts | 236 +++++++++++++--- 29 files changed, 1564 insertions(+), 71 deletions(-) create mode 100644 .changeset/reference-cell-width-enum-relocation-variant-cap.md diff --git a/.changeset/reference-cell-width-enum-relocation-variant-cap.md b/.changeset/reference-cell-width-enum-relocation-variant-cap.md new file mode 100644 index 0000000000..e90f524066 --- /dev/null +++ b/.changeset/reference-cell-width-enum-relocation-variant-cap.md @@ -0,0 +1,75 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): 参考文档表格单元格的两条残留宽度 —— 顶层长枚举移入 `### Allowed Values`,联合变体印数量 (#6225, #6226) + +#5340(PR #6211)把**内联摘要里**的长枚举压掉之后,`content/docs/references/**` 仍有 **27 个超过 +400 字符的类型单元格**。它们不是一种毛病,是两种,机制不同,因此分两条修,一次重生成: + +**#6225 —— 9 个「整格就是一个 `Enum<…>`」的顶层词表。** + +`build-docs.ts` 一直有一条更适合长词表的渲染路径 —— `### Allowed Values` 加每个成员一行项目符号 +—— 但它只在**整个 schema** 是 `type: 'string'` + `enum` 时才走。同样 49 个成员,被 Zod 提升成具名 +schema(`data/FieldType`)的走项目符号,内联在**属性**上的(`Field.type`)得到一个 561 字符的表格 +单元格,而 `ApiError.code` 的 261 个成员得到 **6092 字符**。一份词表落在哪种渲染上,取决于它有没有 +被提升 —— 这是关于 Zod 的事实,不是关于读者怎么读它的事实。 + +新增的 `formatPropertyType` 就是那条分支的镜像,**逐条件对齐**(`type === 'string'` 且 `enum` 是 +数组):属性自己的类型节点就是那份词表。单元格印一个样本加数量,完整成员表由 `build-docs.ts` 印在 +表格正下方 —— **信息一个字都没有离开页面**。省略与搬迁是同一个 `elideEnum` 调用的两个返回值,不可能 +各说各话;预算只在会搬迁的那一个入口读取,`formatType` 本身一行没改,所以 #5340 立下的「词表自己那 +一行永不省略」在 `formatType` 上原样成立。 + +刻意**不**匹配的三种位置(各自都在单元格里渲染出 `Enum<…>`):`Enum<…>[]`、 +`Record>`、以及联合变体(`Enum<…> | string`,`ui/page.mdx` 的 `PageComponent.type`)。 +对它们来说「本属性的允许值」不是实话 —— 成员是**元素 / 值 / 某一个变体**的词表 —— 表格下面挂一份项目 +符号会宣称 schema 没说过的事。数字枚举同样不匹配:项目符号把每个成员渲染成 `` `x` ``,分不出 `2` 与 +`'2'`,那正是 #5729 修掉的错(镜像的整 schema 分支也要求 `type === 'string'`,所以这是对齐不是巧合)。 + +**#6226 —— 另外 18 个里的联合变体重复。** 维护者裁决:变体数上限 + 自报被省略的变体数。 +`formatType` 的 `anyOf` 分支原本是 `variants.map(...).join(' | ')`,对变体之间的相似度一无所知 —— +`ui/app.mdx` 的 `App.navigation` 把同一个 `{ id; label; icon?; order?; … }` 印了 9 遍,其中 7 遍逐字 +相同,582 字符。现在最多印 4 个,其余印 `… +N more`。 + +**阈值全部实测,没有拍脑袋的常数。** 在真实语料上逐个候选值重生成再量: + +- 枚举预算:40 到 176 之间的**每一个**值都给出**完全相同**的宽度结果(>200 为 121、>400 为 18、 + >900 为 1、p99 227、max 1538),所以这段区间里选哪个跟宽度无关,只跟**搬走多少份词表**有关 —— + 从 227 份掉到 24 份。取 **160**:拿到能拿到的最好宽度,只搬 25 份而不是 227 份,距 184 的悬崖还 + 留有余量;它恰好是内联预算 80 的两倍,把两个位置的不对称说明白了 —— 摘掉一份副本几乎免费,把词表 + 搬出它自己那一行要在页面上多开一节。 +- 变体上限:语料共 353 个联合,2 个变体的占 256(72.5%),累计到 4 个变体是 92.6%,之后是薄尾。 + 上限 2 → 8 个宽单元格,3 → 8,**4 → 9**,5 → 11,6 → 18(与不设上限无异)。取 **4**:它是仍能 + 做完几乎全部工作的最松上限(收到 3 只多救回 1 个单元格,却要省略 44 个联合而不是 26 个),并且 + 与读者上一行刚见过的 `INLINE_KEY_LIMIT = 4` 同源 —— 同一个单元格里两个不同的「印几个之后 `…`」 + 才是裁决要避免的那种不一致。 + +**「标记必须挣回自己的位置」守卫**沿用 #5340 并被两条新省略共用。实测(把守卫改成无条件省略再重生成 +对比):它拒绝了 248 个候选省略里的 **54 个**,那 54 个加起来只省 **328 字符** —— 平均每个 6 字符, +而标记本身要花 12–15 字符。其中 **7 个是搬迁候选**,也就是七整节 `### Allowed Values` 本来会为了给 +一个单元格削掉个位数字符而被加进页面。守卫在搬迁上比在 #5340 的内联省略上更要紧:那边一次拒绝省下 +一个标记,这边省下一整节。 + +**全语料重新测量(216 页 / 8541 个单元格):** + +| | 之前 | 之后 | +|---|---|---| +| 超过 200 字符 | 145 | 121 | +| 超过 400 字符 | 27 | **9** | +| 超过 900 字符 | 4 | **1** | +| p99 | 247 | 227 | +| p95 | 145 | 145(不动) | +| max | **6092** | 1538 | + +p95 纹丝不动,是这次改动的重点:普通单元格一个都没有移位。9 份被搬迁的顶层词表连同另外 16 份 160 +字符档的,全部逐条核过完整成员表就在本页(25/25,0 缺失);新加的 25 个 `### Allowed Values: X.y` +标题按 schema + 属性双重限定,全页面唯一(`api/errors.mdx` 上 `EnhancedApiError` 与 `FieldError` 都有 +`code`,只写属性名会撞锚点)。全语料 MDX 编译保持 **216/216**。 + +**没有被这次改动够到的、如实记录:** 剩下 9 个超过 400 字符的单元格里,`ui/page.mdx` 的 `Page.slots` +(1538,本轮新的 max)恰恰**不是**变体重复 —— 它的每个联合都只有 **2** 个变体(`T` 或 `T[]`),宽度 +来自 `INLINE_KEY_LIMIT` 的 4 个键 × 2 个变体 × 每个约 176 字符,任何 ≥2 的变体上限都够不到它。另有 +4 个(`Manifest.capabilities`、`PluginRegistryEntry.capabilities`、 +`GetTranslationsResponse.translations`、`PluginSecurityManifest.permissions`)**根本没有联合**,宽度 +纯粹来自嵌套深度。这是第三种机制,已另立观察单,不在本 PR 范围内。 diff --git a/content/docs/references/ai/solution-blueprint.mdx b/content/docs/references/ai/solution-blueprint.mdx index 205f75103a..215f659a71 100644 --- a/content/docs/references/ai/solution-blueprint.mdx +++ b/content/docs/references/ai/solution-blueprint.mdx @@ -69,13 +69,65 @@ const result = BlueprintAppSchema.parse(data); | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Field machine name (snake_case) | | **label** | `string` | optional | Human-readable field label | -| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| 'datetime' \| 'time' \| 'boolean' \| 'toggle' \| 'select' \| 'multiselect' \| 'radio' \| 'checkboxes' \| 'lookup' \| 'master_detail' \| 'tree' \| 'user' \| 'image' \| 'file' \| 'avatar' \| 'video' \| 'audio' \| 'formula' \| 'summary' \| 'autonumber' \| 'composite' \| 'repeater' \| 'record' \| 'location' \| 'address' \| 'code' \| 'json' \| 'color' \| 'rating' \| 'slider' \| 'signature' \| 'qrcode' \| 'progress' \| 'tags' \| 'vector'>` | ✅ | Field data type | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | ✅ | Field data type | | **required** | `boolean` | optional | Whether the field is required | | **reference** | `string` | optional | Target object name for lookup / master_detail relationship fields | | **options** | `{ label: string; value: string }[]` | optional | Choices for select / multiselect / radio fields | | **summaryOperations** | `{ object: string; function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max'>; field?: string; relationshipField?: string; … }` | optional | REQUIRED when `type` is "summary" (a roll-up of child records: 任务总数 / 报名人数 / 合计金额 / 已完成任务数). Names the child object, the aggregation, and — for a qualified count/sum — the condition. A "summary" field without it materializes runtime-dead. | | **expression** | `string` | optional | REQUIRED when `type` is "formula" — the CEL body the field computes, e.g. "record.quantity * record.unit_price", or "record.order_no + ' · ' + record.customer" for a composed title. A "formula" field without it materializes runtime-dead: the engine builds its formula plan only from fields that HAVE an expression, so the field reads null everywhere, forever. Same failure shape as a "summary" with no `summaryOperations`. Note `nameField` on the object recommends a formula for numbered entities (invoice/ticket) — that formula needs THIS key, or the record title is blank on every card, lookup chip and breadcrumb. | +### Allowed Values: `BlueprintField.type` + +* `text` +* `textarea` +* `email` +* `url` +* `phone` +* `password` +* `secret` +* `markdown` +* `html` +* `richtext` +* `number` +* `currency` +* `percent` +* `date` +* `datetime` +* `time` +* `boolean` +* `toggle` +* `select` +* `multiselect` +* `radio` +* `checkboxes` +* `lookup` +* `master_detail` +* `tree` +* `user` +* `image` +* `file` +* `avatar` +* `video` +* `audio` +* `formula` +* `summary` +* `autonumber` +* `composite` +* `repeater` +* `record` +* `location` +* `address` +* `code` +* `json` +* `color` +* `rating` +* `slider` +* `signature` +* `qrcode` +* `progress` +* `tags` +* `vector` + --- diff --git a/content/docs/references/api/contract.mdx b/content/docs/references/api/contract.mdx index 6bb2d6b155..09120c8658 100644 --- a/content/docs/references/api/contract.mdx +++ b/content/docs/references/api/contract.mdx @@ -27,13 +27,277 @@ const result = ApiErrorSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| 'INVALID_REFERENCE' \| 'DUPLICATE_VALUE' \| 'INVALID_QUERY' \| 'INVALID_FILTER' \| 'INVALID_SORT' \| 'MAX_RECORDS_EXCEEDED' \| 'UNAUTHENTICATED' \| 'INVALID_CREDENTIALS' \| 'EXPIRED_TOKEN' \| 'INVALID_TOKEN' \| 'SESSION_EXPIRED' \| 'MFA_REQUIRED' \| 'EMAIL_NOT_VERIFIED' \| 'PERMISSION_DENIED' \| 'INSUFFICIENT_PRIVILEGES' \| 'FIELD_NOT_ACCESSIBLE' \| 'RECORD_NOT_ACCESSIBLE' \| 'LICENSE_REQUIRED' \| 'IP_RESTRICTED' \| 'TIME_RESTRICTED' \| 'RESOURCE_NOT_FOUND' \| 'OBJECT_NOT_FOUND' \| 'RECORD_NOT_FOUND' \| 'FIELD_NOT_FOUND' \| 'ENDPOINT_NOT_FOUND' \| 'RESOURCE_CONFLICT' \| 'CONCURRENT_MODIFICATION' \| 'DELETE_RESTRICTED' \| 'DUPLICATE_RECORD' \| 'LOCK_CONFLICT' \| 'METHOD_NOT_ALLOWED' \| 'PRECONDITION_REQUIRED' \| 'RATE_LIMIT_EXCEEDED' \| 'QUOTA_EXCEEDED' \| 'CONCURRENT_LIMIT_EXCEEDED' \| 'INTERNAL_ERROR' \| 'DATABASE_ERROR' \| 'TIMEOUT' \| 'SERVICE_UNAVAILABLE' \| 'NOT_IMPLEMENTED' \| 'EXTERNAL_SERVICE_ERROR' \| 'INTEGRATION_ERROR' \| 'WEBHOOK_DELIVERY_FAILED' \| 'BATCH_PARTIAL_FAILURE' \| 'BATCH_COMPLETE_FAILURE' \| 'TRANSACTION_FAILED' \| 'ACCOUNT_LOCKED' \| 'ALREADY_REVERTED' \| 'AMBIGUOUS_MATCH' \| 'ANALYTICS_QUERY_FAILED' \| 'APPROVAL_ACTIONS_FAILED' \| 'APPROVAL_RECALL_FAILED' \| 'APPROVAL_REQUEST_GET_FAILED' \| 'APPROVAL_REQUEST_LIST_FAILED' \| 'ASYNC_NOT_SUPPORTED' \| 'ATTACHMENT_DELETE_DENIED' \| 'ATTACHMENT_DOWNLOAD_DENIED' \| 'ATTACHMENT_PARENT_ACCESS' \| 'AUDIENCE_NOT_ALLOWED' \| 'AUTH_CONFIG_ERROR' \| 'AUTH_REQUIRED' \| 'AUTOMATION_UNSCOPED_RUN_DATA_ACCESS' \| 'BATCH_ABORTED' \| 'BATCH_NOT_ATOMIC' \| 'BATCH_TOO_LARGE' \| 'BATCH_UNRESOLVED_REF' \| 'BLANK_MATCH_KEY' \| 'CLONE_DISABLED' \| 'CLOUD_FETCH_FAILED' \| 'CLOUD_UNCONFIGURED' \| 'COMMIT_NOT_FOUND' \| 'CONCURRENT_UPDATE' \| 'CONFLICTING_MAPPING' \| 'CONNECTOR_UPSTREAM_UNAVAILABLE' \| 'CREATE_FAILED' \| 'CUBE_NOT_FOUND' \| 'DATASET_INVALID' \| 'DATASOURCE_ADMIN_ERROR' \| 'DELEGABLE_SCOPE_FAILED' \| 'DELIVERY_NOT_ELIGIBLE' \| 'DESTRUCTIVE_CHANGE' \| 'DEVICE_CODE_FAILED' \| 'DOMAIN_VERIFICATION_DISABLED' \| 'DOMAIN_VERIFICATION_FAILED' \| 'DRIVER_UNAVAILABLE' \| 'DUPLICATE_REQUEST' \| 'EMAIL_SEND_FAILED' \| 'EMAIL_SERVICE_REQUIRED' \| 'ENQUEUE_FAILED' \| 'ENVIRONMENT_BIND_FAILED' \| 'ENVIRONMENT_NOT_FOUND' \| 'ENV_ACCESS_DENIED' \| 'ERR_BULK_RESULT_MISMATCH' \| 'ERR_DATASOURCE_UNAVAILABLE' \| 'ERR_DRIVER_CONNECT' \| 'ERR_FILE_CONSTRAINT' \| 'ERR_FILE_REFERENCE_COPY' \| 'ERR_READONLY_FIELD_REJECTED' \| 'ERR_SUMMARY_RECOMPUTE' \| 'EXECUTION_ERROR' \| 'EXPIRED_OR_REVOKED' \| 'EXPIRY_IN_PAST' \| 'EXPIRY_TOO_LONG' \| 'EXPLAIN_FAILED' \| 'EXPORT_NOT_PERMITTED' \| 'EXTERNAL_DATASOURCE_ERROR' \| 'EXTERNAL_IMPORT_ERROR' \| 'EXTERNAL_SCHEMA_MISMATCH' \| 'EXTERNAL_SCHEMA_MODE_VIOLATION' \| 'EXTERNAL_WRITE_FORBIDDEN' \| 'FEEDS_DISABLED' \| 'FILES_DISABLED' \| 'FILE_DOWNLOAD_DENIED' \| 'FILE_NOT_FOUND' \| 'FILTER_TOKEN_UNKNOWN' \| 'FILTER_TOKEN_UNRESOLVED' \| 'FORBIDDEN' \| 'FORM_NOT_FOUND' \| 'FORM_RESOLVE_FAILED' \| 'IMPORT_JOB_CREATE_FAILED' \| 'IMPORT_ROW_FAILED' \| 'INTERNAL' \| 'INVALID_EMAIL' \| 'INVALID_EXPIRY' \| 'INVALID_METADATA' \| 'INVALID_OR_EXPIRED' \| 'INVALID_PHONE' \| 'INVALID_REQUEST' \| 'INVALID_RESUME_TOKEN' \| 'INVALID_SIGNAL' \| 'INVALID_SIGNATURE' \| 'INVALID_STATE' \| 'INVITE_EMAIL_FAILED' \| 'INVITE_REQUIRES_EMAIL' \| 'INVITE_SMS_FAILED' \| 'IP_NOT_ALLOWED' \| 'ITEM_LOCKED' \| 'LAST_LOCAL_CREDENTIAL' \| 'LOOKUP_NOT_PUBLIC' \| 'LOOKUP_TARGET_MISSING' \| 'MANIFEST_CONFLICT' \| 'MAPPING_FORMAT_MISMATCH' \| 'MAPPING_FORMAT_UNSUPPORTED' \| 'MAPPING_NOT_FOUND' \| 'MAPPING_TARGET_MISMATCH' \| 'MARKETPLACE_PROXY_FAILED' \| 'MARKETPLACE_STORAGE_FAILED' \| 'MARKETPLACE_UNAVAILABLE' \| 'METADATA_BRANCH' \| 'METADATA_CONFLICT' \| 'METADATA_NOT_FOUND' \| 'METADATA_SCHEMA_INVALID' \| 'MONGODB_MULTI_TENANT_UNSUPPORTED' \| 'NAMESPACE_PREFIX' \| 'NEEDS_PASSWORD' \| 'NODE_FAILURE' \| 'NOTHING_TO_PURGE' \| 'NOT_ATTEMPTED' \| 'NOT_CREATABLE' \| 'NOT_FOUND' \| 'NOT_OVERRIDABLE' \| 'NOT_UNDOABLE' \| 'NO_DRAFT' \| 'NO_EXECUTOR' \| 'NO_IDENTITY' \| 'NO_MATCH' \| 'NO_PENDING_VERIFICATION' \| 'OAUTH_REGISTER_FAILED' \| 'OBJECT_API_DISABLED' \| 'OBJECT_API_METHOD_NOT_ALLOWED' \| 'OPENAPI_UNAVAILABLE' \| 'OS_PROTOCOL_INCOMPATIBLE' \| 'OVERLAY_PERSISTENCE_FAILED' \| 'PACKAGE_DELETE_FAILED' \| 'PACKAGE_DELETE_PARTIAL' \| 'PACKAGE_MANIFEST_INVALID' \| 'PACKAGE_PUBLISH_FAILED' \| 'PASSWORD_ALREADY_SET' \| 'PASSWORD_EXPIRED' \| 'PASSWORD_POLICY_VIOLATION' \| 'PASSWORD_REUSE' \| 'PAYLOAD_TOO_LARGE' \| 'PERMISSION_NOT_ALLOWED' \| 'PHONE_NOT_ENABLED' \| 'PLUGIN_INSTALL_FAILED' \| 'PLUGIN_MANIFEST_INVALID' \| 'PLUGIN_REGISTER_FAILED' \| 'PROJECT_MEMBERSHIP_REQUIRED' \| 'PROJECT_NOT_FOUND' \| 'PROJECT_PROVISIONING' \| 'PROJECT_PROVISIONING_FAILED' \| 'RAW_SQL_UNSUPPORTED' \| 'READ_SCOPE_COMPILE_FAILED' \| 'RECORD_GONE' \| 'RECORD_LOCKED' \| 'REPORTS_LIST_FAILED' \| 'REPORT_DELETE_FAILED' \| 'REPORT_GET_FAILED' \| 'REPORT_NOT_FOUND' \| 'REPORT_RUN_FAILED' \| 'REPORT_SAVE_FAILED' \| 'REPORT_SCHEDULE_FAILED' \| 'REQUEST_NOT_FOUND' \| 'RESEED_NO_ROWS' \| 'RESEED_SKIPPED' \| 'RESUME_FAILED' \| 'RESUME_IN_PROGRESS' \| 'RESUME_TARGET_LOST' \| 'ROLLED_BACK' \| 'ROUTE_NOT_FOUND' \| 'RULE_DEFINE_FAILED' \| 'RULE_DELETE_FAILED' \| 'RULE_EVALUATE_FAILED' \| 'RULE_GET_FAILED' \| 'RULE_LIST_FAILED' \| 'RULE_NOT_FOUND' \| 'RUN_NOT_FOUND' \| 'SAML_REGISTER_FAILED' \| 'SCHEDULES_LIST_FAILED' \| 'SCHEDULE_DELETE_FAILED' \| 'SETTINGS_ACTION_FAILED' \| 'SETTINGS_FORBIDDEN' \| 'SETTINGS_LOCKED' \| 'SETTINGS_UNKNOWN_KEY' \| 'SETTINGS_UNKNOWN_NAMESPACE' \| 'SETTINGS_VALIDATION' \| 'SHARES_LIST_FAILED' \| 'SHARE_GRANT_FAILED' \| 'SHARE_REVOKE_FAILED' \| 'SHARING_NOT_ENABLED' \| 'SIGN_IN_REQUIRED' \| 'SSO_REGISTER_FAILED' \| 'SSO_REGISTER_FORBIDDEN' \| 'STORE_UNAVAILABLE' \| 'SUGGESTION_CONFIRM_FAILED' \| 'SUGGESTION_DISMISS_FAILED' \| 'SUGGESTION_LIST_FAILED' \| 'SUGGESTION_NOT_FOUND' \| 'SUGGESTION_STATE' \| 'SUMMARY_RECOMPUTE_FAILED' \| 'UNAUTHORIZED' \| 'UNIQUE_VIOLATION' \| 'UNKNOWN_KEY' \| 'UNKNOWN_NAMESPACE' \| 'UNSUPPORTED' \| 'UNSUPPORTED_QUERY_PARAM' \| 'UNSUPPORTED_TRANSFORM' \| 'UPLOAD_SESSION_NOT_FOUND' \| 'USER_ALREADY_EXISTS' \| 'VALIDATION_FAILED' \| 'VERSION_NOT_FOUND' \| 'VERSION_NOT_RESTORABLE' \| 'WRITABLE_PACKAGE_REQUIRED' \| 'WRONG_PASSWORD'>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ ERROR_CODE_LEDGER) | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +254 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ ERROR_CODE_LEDGER) | | **message** | `string` | ✅ | Readable error message | | **category** | `string` | optional | Error category (e.g. validation, authorization) | | **httpStatus** | `integer` | optional | HTTP status of the response carrying this error | | **details** | `any` | optional | Additional error context (e.g. field validation errors) | | **requestId** | `string` | optional | Request ID for tracking | +### Allowed Values: `ApiError.code` + +* `VALIDATION_ERROR` +* `INVALID_FIELD` +* `MISSING_REQUIRED_FIELD` +* `INVALID_FORMAT` +* `VALUE_TOO_LONG` +* `VALUE_TOO_SHORT` +* `VALUE_OUT_OF_RANGE` +* `INVALID_REFERENCE` +* `DUPLICATE_VALUE` +* `INVALID_QUERY` +* `INVALID_FILTER` +* `INVALID_SORT` +* `MAX_RECORDS_EXCEEDED` +* `UNAUTHENTICATED` +* `INVALID_CREDENTIALS` +* `EXPIRED_TOKEN` +* `INVALID_TOKEN` +* `SESSION_EXPIRED` +* `MFA_REQUIRED` +* `EMAIL_NOT_VERIFIED` +* `PERMISSION_DENIED` +* `INSUFFICIENT_PRIVILEGES` +* `FIELD_NOT_ACCESSIBLE` +* `RECORD_NOT_ACCESSIBLE` +* `LICENSE_REQUIRED` +* `IP_RESTRICTED` +* `TIME_RESTRICTED` +* `RESOURCE_NOT_FOUND` +* `OBJECT_NOT_FOUND` +* `RECORD_NOT_FOUND` +* `FIELD_NOT_FOUND` +* `ENDPOINT_NOT_FOUND` +* `RESOURCE_CONFLICT` +* `CONCURRENT_MODIFICATION` +* `DELETE_RESTRICTED` +* `DUPLICATE_RECORD` +* `LOCK_CONFLICT` +* `METHOD_NOT_ALLOWED` +* `PRECONDITION_REQUIRED` +* `RATE_LIMIT_EXCEEDED` +* `QUOTA_EXCEEDED` +* `CONCURRENT_LIMIT_EXCEEDED` +* `INTERNAL_ERROR` +* `DATABASE_ERROR` +* `TIMEOUT` +* `SERVICE_UNAVAILABLE` +* `NOT_IMPLEMENTED` +* `EXTERNAL_SERVICE_ERROR` +* `INTEGRATION_ERROR` +* `WEBHOOK_DELIVERY_FAILED` +* `BATCH_PARTIAL_FAILURE` +* `BATCH_COMPLETE_FAILURE` +* `TRANSACTION_FAILED` +* `ACCOUNT_LOCKED` +* `ALREADY_REVERTED` +* `AMBIGUOUS_MATCH` +* `ANALYTICS_QUERY_FAILED` +* `APPROVAL_ACTIONS_FAILED` +* `APPROVAL_RECALL_FAILED` +* `APPROVAL_REQUEST_GET_FAILED` +* `APPROVAL_REQUEST_LIST_FAILED` +* `ASYNC_NOT_SUPPORTED` +* `ATTACHMENT_DELETE_DENIED` +* `ATTACHMENT_DOWNLOAD_DENIED` +* `ATTACHMENT_PARENT_ACCESS` +* `AUDIENCE_NOT_ALLOWED` +* `AUTH_CONFIG_ERROR` +* `AUTH_REQUIRED` +* `AUTOMATION_UNSCOPED_RUN_DATA_ACCESS` +* `BATCH_ABORTED` +* `BATCH_NOT_ATOMIC` +* `BATCH_TOO_LARGE` +* `BATCH_UNRESOLVED_REF` +* `BLANK_MATCH_KEY` +* `CLONE_DISABLED` +* `CLOUD_FETCH_FAILED` +* `CLOUD_UNCONFIGURED` +* `COMMIT_NOT_FOUND` +* `CONCURRENT_UPDATE` +* `CONFLICTING_MAPPING` +* `CONNECTOR_UPSTREAM_UNAVAILABLE` +* `CREATE_FAILED` +* `CUBE_NOT_FOUND` +* `DATASET_INVALID` +* `DATASOURCE_ADMIN_ERROR` +* `DELEGABLE_SCOPE_FAILED` +* `DELIVERY_NOT_ELIGIBLE` +* `DESTRUCTIVE_CHANGE` +* `DEVICE_CODE_FAILED` +* `DOMAIN_VERIFICATION_DISABLED` +* `DOMAIN_VERIFICATION_FAILED` +* `DRIVER_UNAVAILABLE` +* `DUPLICATE_REQUEST` +* `EMAIL_SEND_FAILED` +* `EMAIL_SERVICE_REQUIRED` +* `ENQUEUE_FAILED` +* `ENVIRONMENT_BIND_FAILED` +* `ENVIRONMENT_NOT_FOUND` +* `ENV_ACCESS_DENIED` +* `ERR_BULK_RESULT_MISMATCH` +* `ERR_DATASOURCE_UNAVAILABLE` +* `ERR_DRIVER_CONNECT` +* `ERR_FILE_CONSTRAINT` +* `ERR_FILE_REFERENCE_COPY` +* `ERR_READONLY_FIELD_REJECTED` +* `ERR_SUMMARY_RECOMPUTE` +* `EXECUTION_ERROR` +* `EXPIRED_OR_REVOKED` +* `EXPIRY_IN_PAST` +* `EXPIRY_TOO_LONG` +* `EXPLAIN_FAILED` +* `EXPORT_NOT_PERMITTED` +* `EXTERNAL_DATASOURCE_ERROR` +* `EXTERNAL_IMPORT_ERROR` +* `EXTERNAL_SCHEMA_MISMATCH` +* `EXTERNAL_SCHEMA_MODE_VIOLATION` +* `EXTERNAL_WRITE_FORBIDDEN` +* `FEEDS_DISABLED` +* `FILES_DISABLED` +* `FILE_DOWNLOAD_DENIED` +* `FILE_NOT_FOUND` +* `FILTER_TOKEN_UNKNOWN` +* `FILTER_TOKEN_UNRESOLVED` +* `FORBIDDEN` +* `FORM_NOT_FOUND` +* `FORM_RESOLVE_FAILED` +* `IMPORT_JOB_CREATE_FAILED` +* `IMPORT_ROW_FAILED` +* `INTERNAL` +* `INVALID_EMAIL` +* `INVALID_EXPIRY` +* `INVALID_METADATA` +* `INVALID_OR_EXPIRED` +* `INVALID_PHONE` +* `INVALID_REQUEST` +* `INVALID_RESUME_TOKEN` +* `INVALID_SIGNAL` +* `INVALID_SIGNATURE` +* `INVALID_STATE` +* `INVITE_EMAIL_FAILED` +* `INVITE_REQUIRES_EMAIL` +* `INVITE_SMS_FAILED` +* `IP_NOT_ALLOWED` +* `ITEM_LOCKED` +* `LAST_LOCAL_CREDENTIAL` +* `LOOKUP_NOT_PUBLIC` +* `LOOKUP_TARGET_MISSING` +* `MANIFEST_CONFLICT` +* `MAPPING_FORMAT_MISMATCH` +* `MAPPING_FORMAT_UNSUPPORTED` +* `MAPPING_NOT_FOUND` +* `MAPPING_TARGET_MISMATCH` +* `MARKETPLACE_PROXY_FAILED` +* `MARKETPLACE_STORAGE_FAILED` +* `MARKETPLACE_UNAVAILABLE` +* `METADATA_BRANCH` +* `METADATA_CONFLICT` +* `METADATA_NOT_FOUND` +* `METADATA_SCHEMA_INVALID` +* `MONGODB_MULTI_TENANT_UNSUPPORTED` +* `NAMESPACE_PREFIX` +* `NEEDS_PASSWORD` +* `NODE_FAILURE` +* `NOTHING_TO_PURGE` +* `NOT_ATTEMPTED` +* `NOT_CREATABLE` +* `NOT_FOUND` +* `NOT_OVERRIDABLE` +* `NOT_UNDOABLE` +* `NO_DRAFT` +* `NO_EXECUTOR` +* `NO_IDENTITY` +* `NO_MATCH` +* `NO_PENDING_VERIFICATION` +* `OAUTH_REGISTER_FAILED` +* `OBJECT_API_DISABLED` +* `OBJECT_API_METHOD_NOT_ALLOWED` +* `OPENAPI_UNAVAILABLE` +* `OS_PROTOCOL_INCOMPATIBLE` +* `OVERLAY_PERSISTENCE_FAILED` +* `PACKAGE_DELETE_FAILED` +* `PACKAGE_DELETE_PARTIAL` +* `PACKAGE_MANIFEST_INVALID` +* `PACKAGE_PUBLISH_FAILED` +* `PASSWORD_ALREADY_SET` +* `PASSWORD_EXPIRED` +* `PASSWORD_POLICY_VIOLATION` +* `PASSWORD_REUSE` +* `PAYLOAD_TOO_LARGE` +* `PERMISSION_NOT_ALLOWED` +* `PHONE_NOT_ENABLED` +* `PLUGIN_INSTALL_FAILED` +* `PLUGIN_MANIFEST_INVALID` +* `PLUGIN_REGISTER_FAILED` +* `PROJECT_MEMBERSHIP_REQUIRED` +* `PROJECT_NOT_FOUND` +* `PROJECT_PROVISIONING` +* `PROJECT_PROVISIONING_FAILED` +* `RAW_SQL_UNSUPPORTED` +* `READ_SCOPE_COMPILE_FAILED` +* `RECORD_GONE` +* `RECORD_LOCKED` +* `REPORTS_LIST_FAILED` +* `REPORT_DELETE_FAILED` +* `REPORT_GET_FAILED` +* `REPORT_NOT_FOUND` +* `REPORT_RUN_FAILED` +* `REPORT_SAVE_FAILED` +* `REPORT_SCHEDULE_FAILED` +* `REQUEST_NOT_FOUND` +* `RESEED_NO_ROWS` +* `RESEED_SKIPPED` +* `RESUME_FAILED` +* `RESUME_IN_PROGRESS` +* `RESUME_TARGET_LOST` +* `ROLLED_BACK` +* `ROUTE_NOT_FOUND` +* `RULE_DEFINE_FAILED` +* `RULE_DELETE_FAILED` +* `RULE_EVALUATE_FAILED` +* `RULE_GET_FAILED` +* `RULE_LIST_FAILED` +* `RULE_NOT_FOUND` +* `RUN_NOT_FOUND` +* `SAML_REGISTER_FAILED` +* `SCHEDULES_LIST_FAILED` +* `SCHEDULE_DELETE_FAILED` +* `SETTINGS_ACTION_FAILED` +* `SETTINGS_FORBIDDEN` +* `SETTINGS_LOCKED` +* `SETTINGS_UNKNOWN_KEY` +* `SETTINGS_UNKNOWN_NAMESPACE` +* `SETTINGS_VALIDATION` +* `SHARES_LIST_FAILED` +* `SHARE_GRANT_FAILED` +* `SHARE_REVOKE_FAILED` +* `SHARING_NOT_ENABLED` +* `SIGN_IN_REQUIRED` +* `SSO_REGISTER_FAILED` +* `SSO_REGISTER_FORBIDDEN` +* `STORE_UNAVAILABLE` +* `SUGGESTION_CONFIRM_FAILED` +* `SUGGESTION_DISMISS_FAILED` +* `SUGGESTION_LIST_FAILED` +* `SUGGESTION_NOT_FOUND` +* `SUGGESTION_STATE` +* `SUMMARY_RECOMPUTE_FAILED` +* `UNAUTHORIZED` +* `UNIQUE_VIOLATION` +* `UNKNOWN_KEY` +* `UNKNOWN_NAMESPACE` +* `UNSUPPORTED` +* `UNSUPPORTED_QUERY_PARAM` +* `UNSUPPORTED_TRANSFORM` +* `UPLOAD_SESSION_NOT_FOUND` +* `USER_ALREADY_EXISTS` +* `VALIDATION_FAILED` +* `VERSION_NOT_FOUND` +* `VERSION_NOT_RESTORABLE` +* `WRITABLE_PACKAGE_REQUIRED` +* `WRONG_PASSWORD` + --- diff --git a/content/docs/references/api/errors.mdx b/content/docs/references/api/errors.mdx index 57bcbba85b..4f557fdf35 100644 --- a/content/docs/references/api/errors.mdx +++ b/content/docs/references/api/errors.mdx @@ -40,7 +40,7 @@ const result = EnhancedApiErrorSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| 'INVALID_REFERENCE' \| 'DUPLICATE_VALUE' \| 'INVALID_QUERY' \| 'INVALID_FILTER' \| 'INVALID_SORT' \| 'MAX_RECORDS_EXCEEDED' \| 'UNAUTHENTICATED' \| 'INVALID_CREDENTIALS' \| 'EXPIRED_TOKEN' \| 'INVALID_TOKEN' \| 'SESSION_EXPIRED' \| 'MFA_REQUIRED' \| 'EMAIL_NOT_VERIFIED' \| 'PERMISSION_DENIED' \| 'INSUFFICIENT_PRIVILEGES' \| 'FIELD_NOT_ACCESSIBLE' \| 'RECORD_NOT_ACCESSIBLE' \| 'LICENSE_REQUIRED' \| 'IP_RESTRICTED' \| 'TIME_RESTRICTED' \| 'RESOURCE_NOT_FOUND' \| 'OBJECT_NOT_FOUND' \| 'RECORD_NOT_FOUND' \| 'FIELD_NOT_FOUND' \| 'ENDPOINT_NOT_FOUND' \| 'RESOURCE_CONFLICT' \| 'CONCURRENT_MODIFICATION' \| 'DELETE_RESTRICTED' \| 'DUPLICATE_RECORD' \| 'LOCK_CONFLICT' \| 'METHOD_NOT_ALLOWED' \| 'PRECONDITION_REQUIRED' \| 'RATE_LIMIT_EXCEEDED' \| 'QUOTA_EXCEEDED' \| 'CONCURRENT_LIMIT_EXCEEDED' \| 'INTERNAL_ERROR' \| 'DATABASE_ERROR' \| 'TIMEOUT' \| 'SERVICE_UNAVAILABLE' \| 'NOT_IMPLEMENTED' \| 'EXTERNAL_SERVICE_ERROR' \| 'INTEGRATION_ERROR' \| 'WEBHOOK_DELIVERY_FAILED' \| 'BATCH_PARTIAL_FAILURE' \| 'BATCH_COMPLETE_FAILURE' \| 'TRANSACTION_FAILED'>` | ✅ | Machine-readable error code | +| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +46 more>` | ✅ | Machine-readable error code | | **message** | `string` | ✅ | Human-readable error message | | **category** | `Enum<'validation' \| 'authentication' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'server' \| 'external' \| 'maintenance'>` | optional | Error category | | **httpStatus** | `number` | optional | HTTP status code | @@ -56,6 +56,62 @@ const result = EnhancedApiErrorSchema.parse(data); | **documentation** | `string` | optional | URL to error documentation | | **helpText** | `string` | optional | Suggested actions to resolve the error | +### Allowed Values: `EnhancedApiError.code` + +* `VALIDATION_ERROR` +* `INVALID_FIELD` +* `MISSING_REQUIRED_FIELD` +* `INVALID_FORMAT` +* `VALUE_TOO_LONG` +* `VALUE_TOO_SHORT` +* `VALUE_OUT_OF_RANGE` +* `INVALID_REFERENCE` +* `DUPLICATE_VALUE` +* `INVALID_QUERY` +* `INVALID_FILTER` +* `INVALID_SORT` +* `MAX_RECORDS_EXCEEDED` +* `UNAUTHENTICATED` +* `INVALID_CREDENTIALS` +* `EXPIRED_TOKEN` +* `INVALID_TOKEN` +* `SESSION_EXPIRED` +* `MFA_REQUIRED` +* `EMAIL_NOT_VERIFIED` +* `PERMISSION_DENIED` +* `INSUFFICIENT_PRIVILEGES` +* `FIELD_NOT_ACCESSIBLE` +* `RECORD_NOT_ACCESSIBLE` +* `LICENSE_REQUIRED` +* `IP_RESTRICTED` +* `TIME_RESTRICTED` +* `RESOURCE_NOT_FOUND` +* `OBJECT_NOT_FOUND` +* `RECORD_NOT_FOUND` +* `FIELD_NOT_FOUND` +* `ENDPOINT_NOT_FOUND` +* `RESOURCE_CONFLICT` +* `CONCURRENT_MODIFICATION` +* `DELETE_RESTRICTED` +* `DUPLICATE_RECORD` +* `LOCK_CONFLICT` +* `METHOD_NOT_ALLOWED` +* `PRECONDITION_REQUIRED` +* `RATE_LIMIT_EXCEEDED` +* `QUOTA_EXCEEDED` +* `CONCURRENT_LIMIT_EXCEEDED` +* `INTERNAL_ERROR` +* `DATABASE_ERROR` +* `TIMEOUT` +* `SERVICE_UNAVAILABLE` +* `NOT_IMPLEMENTED` +* `EXTERNAL_SERVICE_ERROR` +* `INTEGRATION_ERROR` +* `WEBHOOK_DELIVERY_FAILED` +* `BATCH_PARTIAL_FAILURE` +* `BATCH_COMPLETE_FAILURE` +* `TRANSACTION_FAILED` + --- @@ -96,12 +152,42 @@ const result = EnhancedApiErrorSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **field** | `string` | ✅ | Field path (supports dot notation) | -| **code** | `Enum<'required' \| 'invalid_type' \| 'invalid_shape' \| 'unknown_field' \| 'invalid_boolean' \| 'invalid_number' \| 'invalid_date' \| 'invalid_time' \| 'invalid_email' \| 'invalid_url' \| 'invalid_phone' \| 'invalid_json' \| 'invalid_format' \| 'min_length' \| 'max_length' \| 'min_value' \| 'max_value' \| 'min_items' \| 'max_items' \| 'invalid_option' \| 'invalid_value' \| 'reference_not_found' \| 'reference_ambiguous' \| 'rule_violation' \| 'json_schema_violation' \| 'invalid_initial_state' \| 'invalid_transition'>` | ✅ | Which constraint the value violated (field-level catalog, ADR-0114) | +| **code** | `Enum<'required' \| 'invalid_type' \| 'invalid_shape' \| 'unknown_field' \| 'invalid_boolean' \| 'invalid_number' \| 'invalid_date' \| 'invalid_time' \| 'invalid_email' \| … +18 more>` | ✅ | Which constraint the value violated (field-level catalog, ADR-0114) | | **message** | `string` | ✅ | Human-readable error message, rendered in the caller’s locale | | **label** | `string` | optional | Field display label in the caller’s locale | | **value** | `any` | optional | The invalid value that was provided | | **constraint** | `Record` | optional | The constraint that was violated, as discrete values (e.g. `{ maxLength: 512, actual: 3000 }`) | +### Allowed Values: `FieldError.code` + +* `required` +* `invalid_type` +* `invalid_shape` +* `unknown_field` +* `invalid_boolean` +* `invalid_number` +* `invalid_date` +* `invalid_time` +* `invalid_email` +* `invalid_url` +* `invalid_phone` +* `invalid_json` +* `invalid_format` +* `min_length` +* `max_length` +* `min_value` +* `max_value` +* `min_items` +* `max_items` +* `invalid_option` +* `invalid_value` +* `reference_not_found` +* `reference_ambiguous` +* `rule_violation` +* `json_schema_violation` +* `invalid_initial_state` +* `invalid_transition` + --- diff --git a/content/docs/references/api/events.mdx b/content/docs/references/api/events.mdx index b0197a8b26..c3179c9ee2 100644 --- a/content/docs/references/api/events.mdx +++ b/content/docs/references/api/events.mdx @@ -94,7 +94,7 @@ const result = BulkDataEventSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique event identifier | -| **type** | `Enum<'metadata.object.created' \| 'metadata.object.updated' \| 'metadata.object.deleted' \| 'metadata.field.created' \| 'metadata.field.updated' \| 'metadata.field.deleted' \| 'metadata.view.created' \| 'metadata.view.updated' \| 'metadata.view.deleted' \| 'metadata.app.created' \| 'metadata.app.updated' \| 'metadata.app.deleted' \| 'metadata.agent.created' \| 'metadata.agent.updated' \| 'metadata.agent.deleted' \| 'metadata.tool.created' \| 'metadata.tool.updated' \| 'metadata.tool.deleted' \| 'metadata.flow.created' \| 'metadata.flow.updated' \| 'metadata.flow.deleted' \| 'metadata.action.created' \| 'metadata.action.updated' \| 'metadata.action.deleted' \| 'metadata.workflow.created' \| 'metadata.workflow.updated' \| 'metadata.workflow.deleted' \| 'metadata.dashboard.created' \| 'metadata.dashboard.updated' \| 'metadata.dashboard.deleted' \| 'metadata.report.created' \| 'metadata.report.updated' \| 'metadata.report.deleted' \| 'metadata.role.created' \| 'metadata.role.updated' \| 'metadata.role.deleted' \| 'metadata.permission.created' \| 'metadata.permission.updated' \| 'metadata.permission.deleted'>` | ✅ | Event type | +| **type** | `Enum<'metadata.object.created' \| 'metadata.object.updated' \| 'metadata.object.deleted' \| 'metadata.field.created' \| 'metadata.field.updated' \| … +34 more>` | ✅ | Event type | | **metadataType** | `string` | ✅ | Metadata type (object, view, agent, etc.) | | **name** | `string` | ✅ | Metadata item name | | **packageId** | `string` | optional | Package ID | @@ -102,6 +102,48 @@ const result = BulkDataEventSchema.parse(data); | **userId** | `string` | optional | User who triggered the event | | **timestamp** | `string` | ✅ | Event timestamp | +### Allowed Values: `MetadataEvent.type` + +* `metadata.object.created` +* `metadata.object.updated` +* `metadata.object.deleted` +* `metadata.field.created` +* `metadata.field.updated` +* `metadata.field.deleted` +* `metadata.view.created` +* `metadata.view.updated` +* `metadata.view.deleted` +* `metadata.app.created` +* `metadata.app.updated` +* `metadata.app.deleted` +* `metadata.agent.created` +* `metadata.agent.updated` +* `metadata.agent.deleted` +* `metadata.tool.created` +* `metadata.tool.updated` +* `metadata.tool.deleted` +* `metadata.flow.created` +* `metadata.flow.updated` +* `metadata.flow.deleted` +* `metadata.action.created` +* `metadata.action.updated` +* `metadata.action.deleted` +* `metadata.workflow.created` +* `metadata.workflow.updated` +* `metadata.workflow.deleted` +* `metadata.dashboard.created` +* `metadata.dashboard.updated` +* `metadata.dashboard.deleted` +* `metadata.report.created` +* `metadata.report.updated` +* `metadata.report.deleted` +* `metadata.role.created` +* `metadata.role.updated` +* `metadata.role.deleted` +* `metadata.permission.created` +* `metadata.permission.updated` +* `metadata.permission.deleted` + --- diff --git a/content/docs/references/api/metadata.mdx b/content/docs/references/api/metadata.mdx index 51a1662cd9..6ca97d4ef0 100644 --- a/content/docs/references/api/metadata.mdx +++ b/content/docs/references/api/metadata.mdx @@ -361,11 +361,40 @@ Metadata query with filtering, sorting, and pagination | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type | +| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| … +11 more>` | ✅ | Metadata type | | **name** | `string` | ✅ | Item name (snake_case) | | **data** | `Record` | ✅ | Metadata payload | | **namespace** | `string` | optional | Optional namespace | +### Allowed Values: `MetadataRegisterRequest.type` + +* `object` +* `field` +* `hook` +* `seed` +* `mapping` +* `view` +* `page` +* `dashboard` +* `app` +* `action` +* `report` +* `dataset` +* `flow` +* `job` +* `datasource` +* `external_catalog` +* `translation` +* `api` +* `email_template` +* `doc` +* `book` +* `permission` +* `position` +* `agent` +* `tool` +* `skill` + --- diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 67317405d1..c6584722e6 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -73,7 +73,7 @@ const result = AddressSchema.parse(data); | :--- | :--- | :--- | :--- | | **name** | `string` | optional | Machine name (snake_case) | | **label** | `string` | optional | Human readable label | -| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| 'datetime' \| 'time' \| 'boolean' \| 'toggle' \| 'select' \| 'multiselect' \| 'radio' \| 'checkboxes' \| 'lookup' \| 'master_detail' \| 'tree' \| 'user' \| 'image' \| 'file' \| 'avatar' \| 'video' \| 'audio' \| 'formula' \| 'summary' \| 'autonumber' \| 'composite' \| 'repeater' \| 'record' \| 'location' \| 'address' \| 'code' \| 'json' \| 'color' \| 'rating' \| 'slider' \| 'signature' \| 'qrcode' \| 'progress' \| 'tags' \| 'vector'>` | ✅ | Field Data Type | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | ✅ | Field Data Type | | **description** | `string` | optional | Tooltip/Help text | | **format** | `string` | optional | Format string (e.g. email, phone) | | **required** | `boolean` | optional | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | @@ -138,6 +138,58 @@ const result = AddressSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Allowed Values: `Field.type` + +* `text` +* `textarea` +* `email` +* `url` +* `phone` +* `password` +* `secret` +* `markdown` +* `html` +* `richtext` +* `number` +* `currency` +* `percent` +* `date` +* `datetime` +* `time` +* `boolean` +* `toggle` +* `select` +* `multiselect` +* `radio` +* `checkboxes` +* `lookup` +* `master_detail` +* `tree` +* `user` +* `image` +* `file` +* `avatar` +* `video` +* `audio` +* `formula` +* `summary` +* `autonumber` +* `composite` +* `repeater` +* `record` +* `location` +* `address` +* `code` +* `json` +* `color` +* `rating` +* `slider` +* `signature` +* `qrcode` +* `progress` +* `tags` +* `vector` + --- diff --git a/content/docs/references/data/validation.mdx b/content/docs/references/data/validation.mdx index 1a499d4e15..059326a44a 100644 --- a/content/docs/references/data/validation.mdx +++ b/content/docs/references/data/validation.mdx @@ -115,8 +115,8 @@ const result = ConditionalValidationSchema.parse(data); | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | | **type** | `'conditional'` | ✅ | | | **when** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL). e.g. P`record.type == 'enterprise'` | -| **then** | `{ name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| [ConditionalValidation](#conditionalvalidation)` | ✅ | Validation rule to apply when condition is true | -| **otherwise** | `{ name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| [ConditionalValidation](#conditionalvalidation)` | optional | Validation rule to apply when condition is false | +| **then** | `{ name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| … +2 more` | ✅ | Validation rule to apply when condition is true | +| **otherwise** | `{ name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| … +2 more` | optional | Validation rule to apply when condition is false | --- diff --git a/content/docs/references/kernel/manifest.mdx b/content/docs/references/kernel/manifest.mdx index 280a8fe2de..b7a54c7a72 100644 --- a/content/docs/references/kernel/manifest.mdx +++ b/content/docs/references/kernel/manifest.mdx @@ -44,7 +44,7 @@ const result = ManifestSchema.parse(data); | **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) | | **capabilities** | `{ implements?: { protocol: object; conformance?: Enum<'full' \| 'partial' \| 'experimental' \| 'deprecated'>; implementedFeatures?: string[]; features?: { name: string; enabled?: boolean; description?: string; sinceVersion?: string; … }[]; … }[]; provides?: { id: string; name: string; description?: string; version: object; … }[]; requires?: { pluginId: string; version: string; optional?: boolean; reason?: string; … }[]; extensionPoints?: { id: string; name: string; description?: string; type: Enum<'action' \| 'hook' \| 'widget' \| 'provider' \| 'transformer' \| 'validator' \| 'decorator'>; … }[]; … }` | optional | Plugin capability declarations for interoperability | | **extensions** | `Record` | optional | Extension points and contributions | -| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: ({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| { id: string; label: string; icon?: string; order?: number; … })[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | +| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: ({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| … +5 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages | | **loading** | `{ strategy?: Enum<'eager' \| 'lazy' \| 'parallel' \| 'deferred' \| 'on-demand'>; preload?: object; codeSplitting?: object; dynamicImport?: object; … }` | optional | Plugin loading and runtime behavior configuration | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | | **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) | diff --git a/content/docs/references/kernel/metadata-plugin.mdx b/content/docs/references/kernel/metadata-plugin.mdx index 511412bbdd..43c7ed896f 100644 --- a/content/docs/references/kernel/metadata-plugin.mdx +++ b/content/docs/references/kernel/metadata-plugin.mdx @@ -197,7 +197,7 @@ const result = MetadataBulkResultSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type identifier | +| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| … +11 more>` | ✅ | 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 | @@ -210,6 +210,35 @@ const result = MetadataBulkResultSchema.parse(data); | **domain** | `Enum<'data' \| 'ui' \| 'automation' \| 'system' \| 'security' \| 'ai'>` | ✅ | Protocol domain | | **actions** | `{ name: string; label: string; objectName?: string; icon?: string; … }[]` | optional | Declarative type-level actions (e.g. datasource "Test connection"), reusing ActionSchema; merged with plugin-registered actions when emitted | +### Allowed Values: `MetadataTypeRegistryEntry.type` + +* `object` +* `field` +* `hook` +* `seed` +* `mapping` +* `view` +* `page` +* `dashboard` +* `app` +* `action` +* `report` +* `dataset` +* `flow` +* `job` +* `datasource` +* `external_catalog` +* `translation` +* `api` +* `email_template` +* `doc` +* `book` +* `permission` +* `position` +* `agent` +* `tool` +* `skill` + --- diff --git a/content/docs/references/kernel/package-artifact.mdx b/content/docs/references/kernel/package-artifact.mdx index c43158ee0a..0f6d0bd247 100644 --- a/content/docs/references/kernel/package-artifact.mdx +++ b/content/docs/references/kernel/package-artifact.mdx @@ -78,7 +78,25 @@ A single file entry within the artifact | :--- | :--- | :--- | :--- | | **path** | `string` | ✅ | Relative file path within the artifact | | **size** | `integer` | ✅ | File size in bytes | -| **category** | `Enum<'objects' \| 'views' \| 'pages' \| 'flows' \| 'dashboards' \| 'permissions' \| 'agents' \| 'reports' \| 'actions' \| 'translations' \| 'themes' \| 'datasets' \| 'apis' \| 'triggers' \| 'workflows'>` | optional | Metadata category this file belongs to | +| **category** | `Enum<'objects' \| 'views' \| 'pages' \| 'flows' \| 'dashboards' \| 'permissions' \| 'agents' \| 'reports' \| 'actions' \| 'translations' \| 'themes' \| 'datasets' \| 'apis' \| … +2 more>` | optional | Metadata category this file belongs to | + +### Allowed Values: `ArtifactFileEntry.category` + +* `objects` +* `views` +* `pages` +* `flows` +* `dashboards` +* `permissions` +* `agents` +* `reports` +* `actions` +* `translations` +* `themes` +* `datasets` +* `apis` +* `triggers` +* `workflows` --- diff --git a/content/docs/references/kernel/plugin-loading.mdx b/content/docs/references/kernel/plugin-loading.mdx index 20bdcf5a6d..2355671f3a 100644 --- a/content/docs/references/kernel/plugin-loading.mdx +++ b/content/docs/references/kernel/plugin-loading.mdx @@ -179,13 +179,30 @@ Plugin loading lifecycle event | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'load-started' \| 'load-completed' \| 'load-failed' \| 'init-started' \| 'init-completed' \| 'init-failed' \| 'preload-started' \| 'preload-completed' \| 'cache-hit' \| 'cache-miss' \| 'hot-reload' \| 'dynamic-load' \| 'dynamic-unload' \| 'dynamic-discover'>` | ✅ | | +| **type** | `Enum<'load-started' \| 'load-completed' \| 'load-failed' \| 'init-started' \| 'init-completed' \| 'init-failed' \| 'preload-started' \| 'preload-completed' \| 'cache-hit' \| … +5 more>` | ✅ | | | **pluginId** | `string` | ✅ | | | **timestamp** | `integer` | ✅ | | | **durationMs** | `integer` | optional | | | **metadata** | `Record` | optional | | | **error** | `{ message: string; code?: string; stack?: string }` | optional | | +### Allowed Values: `PluginLoadingEvent.type` + +* `load-started` +* `load-completed` +* `load-failed` +* `init-started` +* `init-completed` +* `init-failed` +* `preload-started` +* `preload-completed` +* `cache-hit` +* `cache-miss` +* `hot-reload` +* `dynamic-load` +* `dynamic-unload` +* `dynamic-discover` + --- diff --git a/content/docs/references/kernel/plugin-security-advanced.mdx b/content/docs/references/kernel/plugin-security-advanced.mdx index 239276a2e6..9864a98b63 100644 --- a/content/docs/references/kernel/plugin-security-advanced.mdx +++ b/content/docs/references/kernel/plugin-security-advanced.mdx @@ -137,7 +137,7 @@ Scope of permission application | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | Unique permission identifier | -| **resource** | `Enum<'data.object' \| 'data.record' \| 'data.field' \| 'ui.view' \| 'ui.dashboard' \| 'ui.report' \| 'system.config' \| 'system.plugin' \| 'system.api' \| 'system.service' \| 'storage.file' \| 'storage.database' \| 'network.http' \| 'network.websocket' \| 'process.spawn' \| 'process.env'>` | ✅ | Type of resource being accessed | +| **resource** | `Enum<'data.object' \| 'data.record' \| 'data.field' \| 'ui.view' \| 'ui.dashboard' \| 'ui.report' \| 'system.config' \| 'system.plugin' \| 'system.api' \| 'system.service' \| … +6 more>` | ✅ | Type of resource being accessed | | **actions** | `Enum<'create' \| 'read' \| 'update' \| 'delete' \| 'execute' \| 'manage' \| 'configure' \| 'share' \| 'export' \| 'import' \| 'admin'>[]` | ✅ | | | **scope** | `Enum<'global' \| 'tenant' \| 'user' \| 'resource' \| 'plugin'>` | optional | Scope of permission application | | **filter** | `{ resourceIds?: string[]; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; fields?: string[] }` | optional | | @@ -145,6 +145,25 @@ Scope of permission application | **required** | `boolean` | optional | | | **justification** | `string` | optional | Why this permission is needed | +### Allowed Values: `PluginPermission.resource` + +* `data.object` +* `data.record` +* `data.field` +* `ui.view` +* `ui.dashboard` +* `ui.report` +* `system.config` +* `system.plugin` +* `system.api` +* `system.service` +* `storage.file` +* `storage.database` +* `network.http` +* `network.websocket` +* `process.spawn` +* `process.env` + --- diff --git a/content/docs/references/system/collaboration.mdx b/content/docs/references/system/collaboration.mdx index 1899e48547..494bf360be 100644 --- a/content/docs/references/system/collaboration.mdx +++ b/content/docs/references/system/collaboration.mdx @@ -102,7 +102,7 @@ const result = AwarenessEventSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **state** | `{ type: 'lww-register'; value: any; timestamp: string; replicaId: string; … } \| { type: 'g-counter'; counts: Record } \| { type: 'pn-counter'; positive: Record; negative: Record } \| { type: 'or-set'; elements: { value: any; timestamp: string; replicaId: string; uid: string; … }[] } \| { type: 'text'; documentId: string; content: string; operations: { operationId: string; replicaId: string; position: integer; insert?: string; … }[]; … }` | ✅ | Merged CRDT state | +| **state** | `{ type: 'lww-register'; value: any; timestamp: string; replicaId: string; … } \| { type: 'g-counter'; counts: Record } \| { type: 'pn-counter'; positive: Record; negative: Record } \| { type: 'or-set'; elements: { value: any; timestamp: string; replicaId: string; uid: string; … }[] } \| … +1 more` | ✅ | Merged CRDT state | | **conflicts** | `{ type: string; description: string; resolved: boolean }[]` | optional | Conflicts encountered during merge | diff --git a/content/docs/references/system/incident-response.mdx b/content/docs/references/system/incident-response.mdx index fdb134cb32..d9ed052ee1 100644 --- a/content/docs/references/system/incident-response.mdx +++ b/content/docs/references/system/incident-response.mdx @@ -43,7 +43,7 @@ Security incident record per ISO 27001:2022 A.5.24–A.5.28 | **title** | `string` | ✅ | Incident title | | **description** | `string` | ✅ | Detailed incident description | | **severity** | `Enum<'critical' \| 'high' \| 'medium' \| 'low'>` | ✅ | Incident severity level | -| **category** | `Enum<'data_breach' \| 'malware' \| 'unauthorized_access' \| 'denial_of_service' \| 'social_engineering' \| 'insider_threat' \| 'physical_security' \| 'configuration_error' \| 'vulnerability_exploit' \| 'policy_violation' \| 'other'>` | ✅ | Incident category | +| **category** | `Enum<'data_breach' \| 'malware' \| 'unauthorized_access' \| 'denial_of_service' \| 'social_engineering' \| 'insider_threat' \| 'physical_security' \| 'configuration_error' \| … +3 more>` | ✅ | Incident category | | **status** | `Enum<'reported' \| 'triaged' \| 'investigating' \| 'containing' \| 'eradicating' \| 'recovering' \| 'resolved' \| 'closed'>` | ✅ | Current incident status | | **reportedBy** | `string` | ✅ | Reporter user ID or system name | | **reportedAt** | `number` | ✅ | Report timestamp | @@ -58,6 +58,20 @@ Security incident record per ISO 27001:2022 A.5.24–A.5.28 | **relatedChangeRequestIds** | `string[]` | optional | Related change request IDs | | **metadata** | `Record` | optional | Custom metadata key-value pairs | +### Allowed Values: `Incident.category` + +* `data_breach` +* `malware` +* `unauthorized_access` +* `denial_of_service` +* `social_engineering` +* `insider_threat` +* `physical_security` +* `configuration_error` +* `vulnerability_exploit` +* `policy_violation` +* `other` + --- diff --git a/content/docs/references/system/metrics.mdx b/content/docs/references/system/metrics.mdx index 536c01cf7c..ce0e0337e3 100644 --- a/content/docs/references/system/metrics.mdx +++ b/content/docs/references/system/metrics.mdx @@ -115,13 +115,36 @@ Metric definition | **name** | `string` | ✅ | Metric name (snake_case) | | **label** | `string` | optional | Display label | | **type** | `Enum<'counter' \| 'gauge' \| 'histogram' \| 'summary'>` | ✅ | Metric type | -| **unit** | `Enum<'nanoseconds' \| 'microseconds' \| 'milliseconds' \| 'seconds' \| 'minutes' \| 'hours' \| 'days' \| 'bytes' \| 'kilobytes' \| 'megabytes' \| 'gigabytes' \| 'terabytes' \| 'requests_per_second' \| 'events_per_second' \| 'bytes_per_second' \| 'percent' \| 'ratio' \| 'count' \| 'operations' \| 'custom'>` | optional | Metric unit | +| **unit** | `Enum<'nanoseconds' \| 'microseconds' \| 'milliseconds' \| 'seconds' \| 'minutes' \| 'hours' \| 'days' \| 'bytes' \| 'kilobytes' \| 'megabytes' \| 'gigabytes' \| 'terabytes' \| … +8 more>` | optional | Metric unit | | **description** | `string` | optional | Metric description | | **labelNames** | `string[]` | ✅ | Label names | | **histogram** | `{ type: Enum<'linear' \| 'exponential' \| 'explicit'>; linear?: object; exponential?: object; explicit?: object }` | optional | Histogram bucket configuration | | **summary** | `{ quantiles: number[]; maxAge: integer; ageBuckets: integer }` | optional | | | **enabled** | `boolean` | ✅ | | +### Allowed Values: `MetricDefinition.unit` + +* `nanoseconds` +* `microseconds` +* `milliseconds` +* `seconds` +* `minutes` +* `hours` +* `days` +* `bytes` +* `kilobytes` +* `megabytes` +* `gigabytes` +* `terabytes` +* `requests_per_second` +* `events_per_second` +* `bytes_per_second` +* `percent` +* `ratio` +* `count` +* `operations` +* `custom` + --- diff --git a/content/docs/references/system/migration.mdx b/content/docs/references/system/migration.mdx index 23b19d3b7a..a8f0a49a30 100644 --- a/content/docs/references/system/migration.mdx +++ b/content/docs/references/system/migration.mdx @@ -65,8 +65,8 @@ A versioned set of atomic schema migration operations | **author** | `string` | optional | Author who created this migration | | **createdAt** | `string` | optional | ISO 8601 timestamp when the migration was created | | **dependencies** | `{ migrationId: string; package?: string }[]` | optional | Migrations that must run before this one | -| **operations** | `({ type: 'add_field'; objectName: string; fieldName: string; field: object } \| { type: 'modify_field'; objectName: string; fieldName: string; changes: Record } \| { type: 'remove_field'; objectName: string; fieldName: string } \| { type: 'create_object'; object: object } \| { type: 'rename_object'; oldName: string; newName: string } \| { type: 'delete_object'; objectName: string } \| { type: 'execute_sql'; sql: string; description?: string })[]` | ✅ | Ordered list of atomic migration operations | -| **rollback** | `({ type: 'add_field'; objectName: string; fieldName: string; field: object } \| { type: 'modify_field'; objectName: string; fieldName: string; changes: Record } \| { type: 'remove_field'; objectName: string; fieldName: string } \| { type: 'create_object'; object: object } \| { type: 'rename_object'; oldName: string; newName: string } \| { type: 'delete_object'; objectName: string } \| { type: 'execute_sql'; sql: string; description?: string })[]` | optional | Operations to reverse this migration | +| **operations** | `({ type: 'add_field'; objectName: string; fieldName: string; field: object } \| { type: 'modify_field'; objectName: string; fieldName: string; changes: Record } \| { type: 'remove_field'; objectName: string; fieldName: string } \| { type: 'create_object'; object: object } \| … +3 more)[]` | ✅ | Ordered list of atomic migration operations | +| **rollback** | `({ type: 'add_field'; objectName: string; fieldName: string; field: object } \| { type: 'modify_field'; objectName: string; fieldName: string; changes: Record } \| { type: 'remove_field'; objectName: string; fieldName: string } \| { type: 'create_object'; object: object } \| … +3 more)[]` | optional | Operations to reverse this migration | --- diff --git a/content/docs/references/system/settings-manifest.mdx b/content/docs/references/system/settings-manifest.mdx index e4c9f5005b..ee42c1f1f7 100644 --- a/content/docs/references/system/settings-manifest.mdx +++ b/content/docs/references/system/settings-manifest.mdx @@ -112,7 +112,7 @@ const result = ResolvedSettingValueSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'group' \| 'child_pane' \| 'info_banner' \| 'title_value' \| 'text' \| 'textarea' \| 'password' \| 'email' \| 'url' \| 'phone' \| 'number' \| 'toggle' \| 'select' \| 'radio' \| 'multiselect' \| 'slider' \| 'color' \| 'json' \| 'action_button'>` | ✅ | Specifier variant | +| **type** | `Enum<'group' \| 'child_pane' \| 'info_banner' \| 'title_value' \| 'text' \| 'textarea' \| 'password' \| 'email' \| 'url' \| 'phone' \| 'number' \| 'toggle' \| 'select' \| 'radio' \| … +5 more>` | ✅ | Specifier variant | | **id** | `string` | optional | Stable identifier (snake_case) | | **key** | `string` | optional | Storage key (snake_case) | | **label** | `string` | ✅ | Display label | @@ -142,6 +142,28 @@ const result = ResolvedSettingValueSchema.parse(data); | **bannerText** | `string` | optional | Markdown body (info_banner) | | **bannerSeverity** | `Enum<'info' \| 'success' \| 'warning' \| 'error'>` | optional | | +### Allowed Values: `Specifier.type` + +* `group` +* `child_pane` +* `info_banner` +* `title_value` +* `text` +* `textarea` +* `password` +* `email` +* `url` +* `phone` +* `number` +* `toggle` +* `select` +* `radio` +* `multiselect` +* `slider` +* `color` +* `json` +* `action_button` + --- diff --git a/content/docs/references/system/training.mdx b/content/docs/references/system/training.mdx index 67298b818a..ba735cc81a 100644 --- a/content/docs/references/system/training.mdx +++ b/content/docs/references/system/training.mdx @@ -72,7 +72,7 @@ Security training course definition | **id** | `string` | ✅ | Unique course identifier | | **title** | `string` | ✅ | Course title | | **description** | `string` | ✅ | Course description and learning objectives | -| **category** | `Enum<'security_awareness' \| 'data_protection' \| 'incident_response' \| 'access_control' \| 'phishing_awareness' \| 'compliance' \| 'secure_development' \| 'physical_security' \| 'business_continuity' \| 'other'>` | ✅ | Training category | +| **category** | `Enum<'security_awareness' \| 'data_protection' \| 'incident_response' \| 'access_control' \| 'phishing_awareness' \| 'compliance' \| 'secure_development' \| … +3 more>` | ✅ | Training category | | **durationMinutes** | `number` | ✅ | Estimated course duration in minutes | | **mandatory** | `boolean` | ✅ | Whether training is mandatory | | **targetRoles** | `string[]` | ✅ | Target roles or groups | @@ -80,6 +80,19 @@ Security training course definition | **passingScore** | `number` | optional | Minimum passing score percentage | | **version** | `string` | optional | Course content version | +### Allowed Values: `TrainingCourse.category` + +* `security_awareness` +* `data_protection` +* `incident_response` +* `access_control` +* `phishing_awareness` +* `compliance` +* `secure_development` +* `physical_security` +* `business_continuity` +* `other` + --- diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index 8ce39b9ba4..aea4421b6d 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -81,7 +81,7 @@ const result = ActionSchema.parse(data); | **undoable** | `boolean` | optional | Offer an Undo affordance after this single-record update action succeeds. | | **resultDialog** | `{ title?: string; description?: string; acknowledge?: string; format?: Enum<'qrcode' \| 'code-list' \| 'secret' \| 'text' \| 'json'>; … }` | optional | Render API response in a one-shot reveal dialog (suppresses successMessage when set). | | **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL). | -| **requiresFeature** | `Enum<'twoFactor' \| 'passkeys' \| 'magicLink' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| 'oidcProvider' \| 'sso' \| 'ssoEnforced' \| 'deviceAuthorization' \| 'admin' \| 'phoneNumber' \| 'phoneNumberOtp'>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. | +| **requiresFeature** | `Enum<'twoFactor' \| 'passkeys' \| 'magicLink' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| 'oidcProvider' \| 'sso' \| 'ssoEnforced' \| 'deviceAuthorization' \| … +3 more>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. | | **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Boolean or predicate (CEL) — action is disabled when TRUE. | | **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there. | | **shortcut** | `never` | optional | [REMOVED] `action.shortcut` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never triggered anything: no keydown listener feeds ActionEngine.getShortcuts(), and objectui's keyboard stack (useKeyboardShortcuts) is hand-registered and never consults action metadata. Delete the key. For a real shortcut, register the key in the Console keyboard stack and have its handler invoke the action by name. | @@ -104,6 +104,22 @@ const result = ActionSchema.parse(data); | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +### Allowed Values: `Action.requiresFeature` + +* `twoFactor` +* `passkeys` +* `magicLink` +* `organization` +* `multiOrgEnabled` +* `degradedTenancy` +* `oidcProvider` +* `sso` +* `ssoEnforced` +* `deviceAuthorization` +* `admin` +* `phoneNumber` +* `phoneNumberOtp` + --- @@ -148,7 +164,7 @@ const result = ActionSchema.parse(data); | **field** | `string` | optional | Snake case identifier (lowercase with underscores only) | | **objectOverride** | `string` | optional | Snake case identifier (lowercase with underscores only) | | **label** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) | -| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| 'datetime' \| 'time' \| 'boolean' \| 'toggle' \| 'select' \| 'multiselect' \| 'radio' \| 'checkboxes' \| 'lookup' \| 'master_detail' \| 'tree' \| 'user' \| 'image' \| 'file' \| 'avatar' \| 'video' \| 'audio' \| 'formula' \| 'summary' \| 'autonumber' \| 'composite' \| 'repeater' \| 'record' \| 'location' \| 'address' \| 'code' \| 'json' \| 'color' \| 'rating' \| 'slider' \| 'signature' \| 'qrcode' \| 'progress' \| 'tags' \| 'vector'>` | optional | | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | optional | | | **required** | `boolean` | optional | | | **options** | `{ label: string; value: string; visibleWhen?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } }[]` | optional | | | **placeholder** | `string` | optional | | @@ -160,7 +176,75 @@ const result = ActionSchema.parse(data); | **reference** | `string` | optional | Reference target object for inline lookup/master_detail params; mirrors FieldSchema.reference. | | **defaultFromRow** | `boolean` | optional | | | **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Param visibility predicate (CEL); omits the param when false. | -| **requiresFeature** | `Enum<'twoFactor' \| 'passkeys' \| 'magicLink' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| 'oidcProvider' \| 'sso' \| 'ssoEnforced' \| 'deviceAuthorization' \| 'admin' \| 'phoneNumber' \| 'phoneNumberOtp'>` | optional | Public auth feature flag gating this param; lowered into `visible` at parse time. | +| **requiresFeature** | `Enum<'twoFactor' \| 'passkeys' \| 'magicLink' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| 'oidcProvider' \| 'sso' \| 'ssoEnforced' \| 'deviceAuthorization' \| … +3 more>` | optional | Public auth feature flag gating this param; lowered into `visible` at parse time. | + +### Allowed Values: `ActionParam.type` + +* `text` +* `textarea` +* `email` +* `url` +* `phone` +* `password` +* `secret` +* `markdown` +* `html` +* `richtext` +* `number` +* `currency` +* `percent` +* `date` +* `datetime` +* `time` +* `boolean` +* `toggle` +* `select` +* `multiselect` +* `radio` +* `checkboxes` +* `lookup` +* `master_detail` +* `tree` +* `user` +* `image` +* `file` +* `avatar` +* `video` +* `audio` +* `formula` +* `summary` +* `autonumber` +* `composite` +* `repeater` +* `record` +* `location` +* `address` +* `code` +* `json` +* `color` +* `rating` +* `slider` +* `signature` +* `qrcode` +* `progress` +* `tags` +* `vector` + +### Allowed Values: `ActionParam.requiresFeature` + +* `twoFactor` +* `passkeys` +* `magicLink` +* `organization` +* `multiOrgEnabled` +* `degradedTenancy` +* `oidcProvider` +* `sso` +* `ssoEnforced` +* `deviceAuthorization` +* `admin` +* `phoneNumber` +* `phoneNumberOtp` --- diff --git a/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx index 74a4873ef2..6987dd2b8b 100644 --- a/content/docs/references/ui/app.mdx +++ b/content/docs/references/ui/app.mdx @@ -73,7 +73,7 @@ const result = ActionNavItemSchema.parse(data); | **active** | `boolean` | optional | Whether the app is enabled | | **isDefault** | `boolean` | optional | Is default app | | **hidden** | `boolean` | optional | Hide from the App Switcher; the shell surfaces hidden apps via the avatar menu instead | -| **navigation** | `({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| { id: string; label: string; icon?: string; order?: number; … })[]` | optional | Full navigation tree for the app sidebar | +| **navigation** | `({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| … +5 more)[]` | optional | Full navigation tree for the app sidebar | | **areas** | `{ id: string; label: string; icon?: string; description?: string; … }[]` | optional | Navigation areas for partitioning navigation by business domain | | **contextSelectors** | `{ id: string; label: string; icon?: string; optionsSource: object; … }[]` | optional | App-level scope dropdowns whose value is injected into nav items as `{}` template vars | | **homePageId** | `never` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (#4667, #4709, ADR-0049). objectui's console did read it before v17 (`resolveLandingRoute`), so this key had a consumer — it was retired because the capability is better expressed on the navigation item itself than as an ID cross-reference that silently falls back when it dangles. An app's landing page IS its first navigation item (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to rewrite existing sources automatically. | @@ -204,7 +204,7 @@ const result = ActionNavItemSchema.parse(data); | **label** | `string` | ✅ | Area display label | | **icon** | `string` | optional | Area icon name | | **description** | `string` | optional | Area description | -| **navigation** | `({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| { id: string; label: string; icon?: string; order?: number; … })[]` | ✅ | Navigation items within this area | +| **navigation** | `({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| … +5 more)[]` | ✅ | Navigation items within this area | --- @@ -220,7 +220,7 @@ A navigation contribution: a package injecting nav items into an app it does not | **app** | `string` | ✅ | Target app name to contribute navigation into (e.g. "setup") | | **group** | `string` | optional | Target group nav-item id to append into (e.g. "group_integrations"); omit to append at the app top level | | **priority** | `integer` | optional | Merge priority within the target group — lower applied first (matches object extender priority) | -| **items** | `({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| { id: string; label: string; icon?: string; order?: number; … })[]` | ✅ | Navigation items contributed into the target app/group | +| **items** | `({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| … +5 more)[]` | ✅ | Navigation items contributed into the target app/group | --- diff --git a/content/docs/references/ui/bulk-action.mdx b/content/docs/references/ui/bulk-action.mdx index d2c43db4d5..a80dbd0dfe 100644 --- a/content/docs/references/ui/bulk-action.mdx +++ b/content/docs/references/ui/bulk-action.mdx @@ -86,7 +86,7 @@ const result = BulkActionDefSchema.parse(data); | **name** | `string` | ✅ | Param key — becomes params[name] in the patch / action params bag. | | **label** | `string` | optional | Field label in the dialog. Plain string: an authored def is not i18n-resolved (see module header). | | **help** | `string` | optional | Help text under the field. (An ActionParam spells this `helpText` — known divergence, module header.) | -| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| 'datetime' \| 'time' \| 'boolean' \| 'toggle' \| 'select' \| 'multiselect' \| 'radio' \| 'checkboxes' \| 'lookup' \| 'master_detail' \| 'tree' \| 'user' \| 'image' \| 'file' \| 'avatar' \| 'video' \| 'audio' \| 'formula' \| 'summary' \| 'autonumber' \| 'composite' \| 'repeater' \| 'record' \| 'location' \| 'address' \| 'code' \| 'json' \| 'color' \| 'rating' \| 'slider' \| 'signature' \| 'qrcode' \| 'progress' \| 'tags' \| 'vector'>` | ✅ | Field widget to render, from the standard field-type vocabulary (text/number/select/lookup/date/…). | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | ✅ | Field widget to render, from the standard field-type vocabulary (text/number/select/lookup/date/…). | | **required** | `boolean` | optional | Blocks the Confirm button until a value is present. | | **default** | `any` | optional | Value applied when the dialog opens. (An ActionParam spells this `defaultValue`.) | | **options** | `({ label: string; value: string \| number \| boolean } & Record)[]` | optional | Static options for select-style widgets. Each entry is `{ label, value }` plus any extra widget config — the entry is open (`.passthrough()`) because the renderer forwards unknown option keys to the field widget, which reads `color` / `icon` / `disabled` / `visibleWhen` beyond the declared pair. | @@ -95,6 +95,58 @@ const result = BulkActionDefSchema.parse(data); | **multiple** | `boolean` | optional | Allow picking multiple values — the param value becomes an array and is written to the patch as-is. | | **placeholder** | `string` | optional | Placeholder text. | +### Allowed Values: `BulkActionParam.type` + +* `text` +* `textarea` +* `email` +* `url` +* `phone` +* `password` +* `secret` +* `markdown` +* `html` +* `richtext` +* `number` +* `currency` +* `percent` +* `date` +* `datetime` +* `time` +* `boolean` +* `toggle` +* `select` +* `multiselect` +* `radio` +* `checkboxes` +* `lookup` +* `master_detail` +* `tree` +* `user` +* `image` +* `file` +* `avatar` +* `video` +* `audio` +* `formula` +* `summary` +* `autonumber` +* `composite` +* `repeater` +* `record` +* `location` +* `address` +* `code` +* `json` +* `color` +* `rating` +* `slider` +* `signature` +* `qrcode` +* `progress` +* `tags` +* `vector` + --- diff --git a/content/docs/references/ui/chart.mdx b/content/docs/references/ui/chart.mdx index 2f70dedb7e..9645e8ccab 100644 --- a/content/docs/references/ui/chart.mdx +++ b/content/docs/references/ui/chart.mdx @@ -96,7 +96,7 @@ Inline aggregation for an object-bound chart | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>` | ✅ | | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| … +6 more>` | ✅ | | | **title** | `string` | optional | Chart title | | **subtitle** | `string` | optional | Chart subtitle | | **description** | `string` | optional | Accessibility description — announced to screen readers as the chart’s label | @@ -111,6 +111,29 @@ Inline aggregation for an object-bound chart | **interaction** | `{ tooltips: boolean; brush: boolean }` | optional | Interaction toggles: `{ tooltips?, brush? }` | | **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Allowed Values: `ChartConfig.type` + +* `bar` +* `horizontal-bar` +* `column` +* `line` +* `area` +* `pie` +* `donut` +* `funnel` +* `scatter` +* `treemap` +* `sankey` +* `combo` +* `gauge` +* `solid-gauge` +* `metric` +* `kpi` +* `bullet` +* `radar` +* `table` +* `pivot` + --- @@ -179,7 +202,7 @@ Type: `string` | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Field name or series identifier | | **label** | `string` | optional | Series display label | -| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>` | optional | Override chart type for this series | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| … +6 more>` | optional | Override chart type for this series | | **color** | `string` | optional | Series color (hex/rgb/token) | | **stack** | `string` | optional | Stack identifier to group series | | **yAxis** | `Enum<'left' \| 'right'>` | ✅ | Bind to specific Y-Axis | @@ -187,6 +210,29 @@ Type: `string` | **dashArray** | `string` | optional | SVG stroke-dasharray override | | **opacity** | `number` | optional | Series opacity override | +### Allowed Values: `ChartSeries.type` + +* `bar` +* `horizontal-bar` +* `column` +* `line` +* `area` +* `pie` +* `donut` +* `funnel` +* `scatter` +* `treemap` +* `sankey` +* `combo` +* `gauge` +* `solid-gauge` +* `metric` +* `kpi` +* `bullet` +* `radar` +* `table` +* `pivot` + --- diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index f9ec108572..9dfc88c073 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -93,7 +93,7 @@ Dashboard header action | **id** | `string` | ✅ | Unique widget identifier (snake_case) | | **title** | `string` | optional | Widget title | | **description** | `string` | optional | Widget description text below the header | -| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>` | ✅ | Visualization type | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| … +6 more>` | ✅ | Visualization type | | **chartConfig** | `{ type: Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| … +13 more>; title?: string; subtitle?: string; description?: string; … }` | optional | Chart visualization configuration | | **colorVariant** | `Enum<'default' \| 'blue' \| 'teal' \| 'orange' \| 'purple' \| 'success' \| 'warning' \| 'danger'>` | optional | Widget color variant for theming | | **requiresObject** | `string` | optional | Hide the widget unless the named object is registered | @@ -113,6 +113,29 @@ Dashboard header action | **responsive** | `never` | optional | [REMOVED] `dashboard.widgets[].responsive` was removed in @objectstack/spec 17.0.0 (#4876, ADR-0049 D2) — no renderer ever read it, so per-widget breakpoint overrides were never applied: the value parsed, validated, and then did nothing. The dashboard grid reflows by its own layout rules (`columns` + `gap` on the dashboard, the `layout` box on each widget). Delete the key. The shared `ResponsiveConfig` shape is NOT gone — it stays live on `page.components[].responsive`, which objectui `useResponsiveConfig` really does read; move the layout there if you need breakpoint behaviour today. Run `os migrate meta --from 16` to rewrite it automatically. | | **aria** | `never` | optional | [REMOVED] `dashboard.widgets[].aria` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 D2) — no renderer ever applied it, so ARIA attributes declared on a widget silently did not reach the DOM: the key promised accessibility compliance it did not deliver. This is the same removal the dashboard-level `aria` got in 17.0.0 (#3896). Delete the key. The dashboard renderer emits its own `aria-*` attributes for the widget grid; author a `title` (and `description`) on the widget instead — those ARE what the renderer labels the card with. The shared `AriaProps` shape is NOT gone: it stays live on `app.aria` and `page.components[].aria`. Run `os migrate meta --from 16` to rewrite it automatically. | +### Allowed Values: `DashboardWidget.type` + +* `bar` +* `horizontal-bar` +* `column` +* `line` +* `area` +* `pie` +* `donut` +* `funnel` +* `scatter` +* `treemap` +* `sankey` +* `combo` +* `gauge` +* `solid-gauge` +* `metric` +* `kpi` +* `bullet` +* `radar` +* `table` +* `pivot` + --- diff --git a/content/docs/references/ui/report.mdx b/content/docs/references/ui/report.mdx index c73554b5b0..8174b4c02b 100644 --- a/content/docs/references/ui/report.mdx +++ b/content/docs/references/ui/report.mdx @@ -81,7 +81,7 @@ const result = JoinedReportBlockSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'radar' \| 'table' \| 'pivot'>` | ✅ | | +| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| … +6 more>` | ✅ | | | **title** | `string` | optional | Chart title | | **subtitle** | `string` | optional | Chart subtitle | | **description** | `string` | optional | Accessibility description — announced to screen readers as the chart’s label | @@ -96,6 +96,29 @@ const result = JoinedReportBlockSchema.parse(data); | **interaction** | `{ tooltips: boolean; brush: boolean }` | optional | Interaction toggles: `{ tooltips?, brush? }` | | **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | +### Allowed Values: `ReportChart.type` + +* `bar` +* `horizontal-bar` +* `column` +* `line` +* `area` +* `pie` +* `donut` +* `funnel` +* `scatter` +* `treemap` +* `sankey` +* `combo` +* `gauge` +* `solid-gauge` +* `metric` +* `kpi` +* `bullet` +* `radar` +* `table` +* `pivot` + --- diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 103ebb5534..17785b4049 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -136,7 +136,7 @@ Column footer summary configuration | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **field** | `string` | ✅ | Field name (snake_case) | -| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| 'datetime' \| 'time' \| 'boolean' \| 'toggle' \| 'select' \| 'multiselect' \| 'radio' \| 'checkboxes' \| 'lookup' \| 'master_detail' \| 'tree' \| 'user' \| 'image' \| 'file' \| 'avatar' \| 'video' \| 'audio' \| 'formula' \| 'summary' \| 'autonumber' \| 'composite' \| 'repeater' \| 'record' \| 'location' \| 'address' \| 'code' \| 'json' \| 'color' \| 'rating' \| 'slider' \| 'signature' \| 'qrcode' \| 'progress' \| 'tags' \| 'vector'>` | optional | Field type (auto-infers widget if omitted) | +| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | optional | Field type (auto-infers widget if omitted) | | **options** | `{ label: string; value: string; color?: string; default?: boolean; … }[]` | optional | Options for select/multiselect/radio/checkboxes fields | | **reference** | `string` | optional | Target object name for lookup/master_detail fields | | **maxLength** | `number` | optional | Maximum character length (for text/textarea/email/url/phone) | @@ -164,6 +164,58 @@ Column footer summary configuration | **disclosure** | `Enum<'inline' \| 'popover'>` | optional | Composite rendering: inline bordered box (default) or a summary line + gear popover (progressive disclosure). | | **fields** | `[FormField](#formfield)[]` | optional | Sub-fields for composite/repeater/record types | +### Allowed Values: `FormField.type` + +* `text` +* `textarea` +* `email` +* `url` +* `phone` +* `password` +* `secret` +* `markdown` +* `html` +* `richtext` +* `number` +* `currency` +* `percent` +* `date` +* `datetime` +* `time` +* `boolean` +* `toggle` +* `select` +* `multiselect` +* `radio` +* `checkboxes` +* `lookup` +* `master_detail` +* `tree` +* `user` +* `image` +* `file` +* `avatar` +* `video` +* `audio` +* `formula` +* `summary` +* `autonumber` +* `composite` +* `repeater` +* `record` +* `location` +* `address` +* `code` +* `json` +* `color` +* `rating` +* `slider` +* `signature` +* `qrcode` +* `progress` +* `tags` +* `vector` + --- @@ -776,9 +828,31 @@ View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **field** | `string` | ✅ | Field name to filter on | -| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| 'ends_with' \| 'greater_than' \| 'less_than' \| 'greater_than_or_equal' \| 'less_than_or_equal' \| 'in' \| 'not_in' \| 'is_empty' \| 'is_not_empty' \| 'is_null' \| 'is_not_null' \| 'before' \| 'after' \| 'between'>` | ✅ | Filter operator | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'starts_with' \| 'ends_with' \| 'greater_than' \| 'less_than' \| 'greater_than_or_equal' \| … +10 more>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value | +### Allowed Values: `ViewFilterRule.operator` + +* `equals` +* `not_equals` +* `contains` +* `not_contains` +* `starts_with` +* `ends_with` +* `greater_than` +* `less_than` +* `greater_than_or_equal` +* `less_than_or_equal` +* `in` +* `not_in` +* `is_empty` +* `is_not_empty` +* `is_null` +* `is_not_null` +* `before` +* `after` +* `between` + --- diff --git a/packages/spec/scripts/build-docs.ts b/packages/spec/scripts/build-docs.ts index 067392a671..91de2f9910 100644 --- a/packages/spec/scripts/build-docs.ts +++ b/packages/spec/scripts/build-docs.ts @@ -36,7 +36,7 @@ import { } from './lib/docs-import-surface'; import { escapeMdxDescription } from './lib/escape-mdx'; import { renderFileDescription } from './lib/file-description'; -import { anchorFor, formatType, type TypeContext } from './lib/format-type'; +import { anchorFor, formatPropertyType, formatType, type TypeContext } from './lib/format-type'; import { createSink } from './lib/generated-output'; import { blurbCoverage, @@ -333,14 +333,22 @@ function generateMarkdown(schemaName: string, schema: any, category: string, zod const typeCtx: TypeContext = { defs, currentSchema: schemaName, schemaHref: schemaHrefFrom(category) }; const renderProperties = (props: any, required: Set = new Set()) => { + // Vocabularies too wide for their own table cell. Collected while the + // table is built and printed as `### Allowed Values` bullets right after + // it, so the complete list never leaves the page the cell sits on + // (#6225) — the same rendering a hoisted `type: 'string'` + `enum` schema + // has always got, now reachable from a property position too. + const relocated: Array<{ key: string; members: string[] }> = []; let t = `### Properties\n\n`; t += `| Property | Type | Required | Description |\n`; t += `| :--- | :--- | :--- | :--- |\n`; for (const [key, prop] of Object.entries(props) as [string, any][]) { + const { cell, allowedValues } = formatPropertyType(prop, typeCtx); + if (allowedValues) relocated.push({ key, members: allowedValues }); // Backslashes first, then pipes — same order as `desc` below, and for // the same reason: escaping pipes first lets a literal backslash in // the input pair with the escape and free the pipe again. - const typeStr = formatType(prop, typeCtx) + const typeStr = cell .replace(/\\/g, '\\\\') .replace(/\|/g, '\\|'); const isReq = required.has(key) ? '✅' : 'optional'; @@ -353,7 +361,16 @@ function generateMarkdown(schemaName: string, schema: any, category: string, zod .replace(/\|/g, '\\|'); t += `| **${key}** | \`${typeStr}\` | ${isReq} | ${desc} |\n`; } - return t + '\n'; + t += '\n'; + // Qualified by schema AND property: `api/errors.mdx` carries a wide + // `code` on both `EnhancedApiError` and `FieldError`, so a heading naming + // only the property would give one page two identical anchors. + for (const { key, members } of relocated) { + t += `### Allowed Values: \`${schemaName}.${key}\`\n\n`; + t += members.map(m => `* \`${m}\``).join('\n'); + t += `\n\n`; + } + return t; }; if (mainDef.type === 'object' && mainDef.properties) { diff --git a/packages/spec/scripts/format-type.test.ts b/packages/spec/scripts/format-type.test.ts index a680e4dc33..aa241395c9 100644 --- a/packages/spec/scripts/format-type.test.ts +++ b/packages/spec/scripts/format-type.test.ts @@ -31,7 +31,7 @@ import { describe, expect, it } from 'vitest'; -import { formatType, type TypeContext } from './lib/format-type'; +import { formatPropertyType, formatType, type TypeContext } from './lib/format-type'; const ctx = (defs: Record = {}): TypeContext => ({ defs, @@ -964,3 +964,252 @@ describe('formatType — the elision boundary (#5340)', () => { ); }); }); + +/** + * `build-docs.ts` has always had a rendering for a long vocabulary — a + * `### Allowed Values` heading and one bullet per member — but it fired only + * when the WHOLE SCHEMA was `type: 'string'` + `enum`. The identical 49 members + * inlined onto a PROPERTY got a 561-character table cell instead, and + * `ApiError.code` got 6092 (#6225). `formatPropertyType` is the mirror of that + * branch, matched to it condition for condition. + * + * REVERSE VERIFICATION, direction predicted BEFORE running. One prediction held + * and one was WRONG in an informative direction — both recorded as measured: + * + * 1. Removing the limb (making `formatPropertyType` always delegate to + * `formatType`) was predicted to redden only PART of this block — the + * relocation cases — while the cases asserting `allowedValues === null` + * stay green, because "no relocation anywhere" is exactly what they + * already assert. HELD: **4 failed | 64 passed** in the file, the four + * reds all relocation cases, every other block green. + * 2. The one that actually validates the design: putting the same budget in + * `formatType`'s `enum` branch — where a naive fix would put it — was + * predicted to redden five PRE-EXISTING #5340 tests, the four + * `NOT elided` cases plus `does not elide when no ctx is passed at all`, + * because a bare `formatType` call has nowhere to relocate members TO. + * MISSED, and low: those five did redden, but so did two of the guards in + * THIS block — `does NOT relocate an enum reached through an array, a + * record or a union variant` and `does NOT relocate a NUMERIC enum`. + * Measured **7 failed | 61 passed**, not the 5 predicted. The two extra + * reds are the useful part of the result: the misplacement is caught by + * this block on its own, so the contract does not depend on #5340's tests + * continuing to exist. That is why the budget is read from + * `formatPropertyType` and never from `formatType` — the narrow entry + * point is the guarantee that a vocabulary is only ever cut where the full + * list is printed underneath it. + */ +describe('formatPropertyType — a property that IS a vocabulary relocates it (#6225)', () => { + it('cuts the `Field.type` cell to a sample and hands back all 49 members', () => { + const { cell, allowedValues } = formatPropertyType({ type: 'string', enum: FIELD_TYPES }, ctx()); + // The cell states what it is — a sample of a 49-term vocabulary — never a + // silent prefix. + expect(cell).toMatch(/^Enum<'text' \| .* \| … \+\d+ more>$/); + expect(cell.length).toBeLessThan(200); + // …and nothing is lost: the caller gets the WHOLE list, in schema order. + expect(allowedValues).toEqual(FIELD_TYPES); + }); + + it('cuts the 261-member `ApiError.code` cell — the 6092-character filed instance', () => { + const { cell, allowedValues } = formatPropertyType({ type: 'string', enum: ERROR_CODES }, ctx()); + expect(cell).toContain('… +'); + expect(cell).toContain('more>'); + expect(cell.length).toBeLessThan(200); + expect(allowedValues).toHaveLength(ERROR_CODES.length); + expect(allowedValues).toEqual(ERROR_CODES); + }); + + it('states the hidden count exactly — shown members plus the count is the whole vocabulary', () => { + const { cell, allowedValues } = formatPropertyType({ type: 'string', enum: FIELD_TYPES }, ctx()); + const hidden = Number(/… \+(\d+) more/.exec(cell)![1]); + const shown = cell.slice('Enum<'.length, -1).split(' | ').filter(m => !m.startsWith('…')).length; + expect(shown + hidden).toBe(FIELD_TYPES.length); + expect(allowedValues).toHaveLength(shown + hidden); + }); + + it('leaves an ordinary short vocabulary spelled out in its own cell', () => { + // The overwhelming majority of the corpus's 893 top-level enums. Nothing + // moves, and no page grows a section for two members. + const { cell, allowedValues } = formatPropertyType( + { type: 'string', enum: ['asc', 'desc'] }, + ctx(), + ); + expect(cell).toBe("Enum<'asc' | 'desc'>"); + expect(allowedValues).toBeNull(); + }); + + it('does NOT relocate an enum reached through an array, a record or a union variant', () => { + // Each renders an `Enum<…>` somewhere in its cell, but "the allowed values + // of this property" would be a false statement for all three — the members + // are the ELEMENT / VALUE / one-variant vocabulary. A bullet list under the + // table would claim something the schema does not, so they keep the full + // spelling #5340 left them with. + const array = formatPropertyType({ type: 'array', items: { type: 'string', enum: FIELD_TYPES } }, ctx()); + expect(array.allowedValues).toBeNull(); + expect(array.cell).toContain("'vector'"); + + const record = formatPropertyType( + { type: 'object', additionalProperties: { type: 'string', enum: FIELD_TYPES } }, + ctx(), + ); + expect(record.allowedValues).toBeNull(); + expect(record.cell).toContain("'vector'"); + + // `ui/page.mdx`'s `PageComponent.type` — `z.union([z.enum([…]), z.string()])`. + const variant = formatPropertyType( + { anyOf: [{ type: 'string', enum: FIELD_TYPES }, { type: 'string' }] }, + ctx(), + ); + expect(variant.allowedValues).toBeNull(); + expect(variant.cell).toContain("'vector'"); + }); + + it('does NOT relocate a NUMERIC enum — the bullets would re-quote it (#5729)', () => { + // The mirrored whole-schema branch is `type === 'string' && enum` too, and + // the match is deliberate rather than incidental: the bullet list renders + // each member as `` * `x` ``, which cannot distinguish `2` from `'2'`. A + // numeric vocabulary keeps its cell, where `formatLiteral` still prints it + // bare. + const { cell, allowedValues } = formatPropertyType( + { type: 'number', enum: Array.from({ length: 60 }, (_, i) => i * 1000) }, + ctx(), + ); + expect(allowedValues).toBeNull(); + expect(cell).toContain('59000'); + expect(cell).not.toContain("'59000'"); + }); + + // Distinct members of a FIXED width (10 characters, 12 once quoted), so the + // boundary arithmetic below is exact. `sized` above cannot serve here: its + // members grow from 10 to 19 characters once the index reaches two digits, + // which is invisible at the 80-character budget it was written for and moves + // the boundary by 9 characters at 160. + const fixed = (n: number) => Array.from({ length: n }, (_, i) => String(i).padStart(10, '0')); + + it('prints a body AT the 160-character budget whole', () => { + // 10 members x 12 chars + 9 separators = 147; an 11th would be 162. + expect(fixed(10).map(m => `'${m}'`).join(' | ').length).toBe(147); + const { cell, allowedValues } = formatPropertyType({ type: 'string', enum: fixed(10) }, ctx()); + expect(cell).not.toContain('more'); + expect(allowedValues).toBeNull(); + }); + + it('prints a body just OVER the budget whole — and adds no page section for it', () => { + // 162 characters. Hiding the single overflowing member saves 3 while the + // marker costs 12, so the guard refuses. The relocation refuses WITH it: + // this is the case where a whole `### Allowed Values` section would have + // been added to a page to shave 3 characters off one cell. Measured on the + // corpus, the guard refuses 7 such sections. + expect(fixed(11).map(m => `'${m}'`).join(' | ').length).toBe(162); + const { cell, allowedValues } = formatPropertyType({ type: 'string', enum: fixed(11) }, ctx()); + expect(cell).not.toContain('more'); + expect(allowedValues).toBeNull(); + }); + + it('relocates at the first width where the marker DOES pay for itself', () => { + // 177 characters: 2 members hidden, 18 saved against a 12-character marker. + expect(fixed(12).map(m => `'${m}'`).join(' | ').length).toBe(177); + const { cell, allowedValues } = formatPropertyType({ type: 'string', enum: fixed(12) }, ctx()); + expect(cell).toContain('… +2 more>'); + expect(allowedValues).toEqual(fixed(12)); + }); + + it('keeps every other property rendering byte-identical to `formatType`', () => { + // The wrapper is a narrow addition, not a second renderer: anything that is + // not an over-wide string vocabulary must come back exactly as before. + for (const node of [ + { type: 'string' }, + { type: 'array', items: { type: 'string' } }, + INDEX_SCHEMA, + BULK_ACTION_PARAMS, + { anyOf: [{ type: 'string' }, { type: 'number' }] }, + { not: {} }, + ]) { + expect(formatPropertyType(node, ctx()).cell).toBe(formatType(node, ctx())); + expect(formatPropertyType(node, ctx()).allowedValues).toBeNull(); + } + }); +}); + +/** + * A union's width is variant COUNT times variant WIDTH, so neither enum budget + * can reach it — `ui/app.mdx`'s `App.navigation` printed the same + * `{ id; label; icon?; order?; … }` shape nine times, seven of them + * character-identical, for 582 characters (#6226). + * + * REVERSE VERIFICATION: raising `VARIANT_LIMIT` past every union in the corpus + * (i.e. putting the un-capped `variants.map(...).join(' | ')` back) was + * predicted to redden only the cases that assert a marker, leaving the three + * that assert a FULL spelling green — because no-cap produces exactly the full + * spelling those three demand. HELD: **4 failed | 64 passed** in the file, the + * four reds being precisely the marker cases, and no other block moved. + */ +describe('formatType — a union spells four variants and counts the rest (#6226)', () => { + const variant = (id: string) => ({ + type: 'object', + properties: { id: { type: 'string' }, label: { type: 'string' }, icon: { type: 'string' }, order: { type: 'number' }, [id]: { type: 'string' } }, + required: ['id', 'label'], + }); + + it('renders the `App.navigation` cell as four variants plus a count (the filed instance)', () => { + const rendered = formatType( + { type: 'array', items: { anyOf: Array.from({ length: 9 }, (_, i) => variant(`k${i}`)) } }, + ctx(), + ); + expect(rendered).toContain('… +5 more'); + // Still an ARRAY of that union — the parenthesis #5338 added must survive + // the elision, or the cell states a different type than the schema. + expect(rendered.startsWith('(')).toBe(true); + expect(rendered.endsWith(')[]')).toBe(true); + expect(rendered.length).toBeLessThan(400); + }); + + it('states the hidden count exactly — shown variants plus the count is the arity', () => { + const rendered = formatType({ anyOf: Array.from({ length: 7 }, (_, i) => variant(`k${i}`)) }, ctx()); + const hidden = Number(/… \+(\d+) more/.exec(rendered)![1]); + const shown = rendered.split(' | ').filter(v => !v.startsWith('…')).length; + expect(shown).toBe(4); + expect(shown + hidden).toBe(7); + }); + + it('spells a union of exactly four variants in full — the cap is not a cliff at four', () => { + const rendered = formatType({ anyOf: Array.from({ length: 4 }, (_, i) => variant(`k${i}`)) }, ctx()); + expect(rendered).not.toContain('more'); + expect(rendered.split(' | ')).toHaveLength(4); + }); + + it('leaves the corpus-dominant two-variant union exactly as it renders today', () => { + // 256 of the corpus's 353 unions are these, and none of them moves. + expect(formatType({ anyOf: [{ type: 'string' }, { type: 'number' }] }, ctx())).toBe( + 'string | number', + ); + }); + + it('does NOT elide five tiny variants — the marker would cost more than it saves', () => { + // `string | string | string | string | string` is 42 characters; capping it + // would print 33 + a 12-character marker footprint and come out LONGER. The + // pay-for-your-marker guard is shared with the enum elisions, so a count + // never replaces a spelling that was already shorter than the count. + const rendered = formatType({ anyOf: Array.from({ length: 5 }, () => ({ type: 'string' })) }, ctx()); + expect(rendered).toBe('string | string | string | string | string'); + expect(rendered).not.toContain('more'); + }); + + it('elides the same way under `oneOf` as under `anyOf`', () => { + const rendered = formatType({ oneOf: Array.from({ length: 9 }, (_, i) => variant(`k${i}`)) }, ctx()); + expect(rendered).toContain('… +5 more'); + }); + + it('caps a union nested inside a shape summary too, alongside the enum elision', () => { + // The two elisions compose: `Page.slots`-shaped cells carry both. + const rendered = formatType( + { + type: 'object', + properties: { + slot: { anyOf: Array.from({ length: 6 }, (_, i) => variant(`k${i}`)) }, + }, + }, + ctx(), + ); + expect(rendered).toContain('… +2 more'); + }); +}); diff --git a/packages/spec/scripts/lib/format-type.ts b/packages/spec/scripts/lib/format-type.ts index bfd355333a..aa235b7fca 100644 --- a/packages/spec/scripts/lib/format-type.ts +++ b/packages/spec/scripts/lib/format-type.ts @@ -50,6 +50,21 @@ export interface TypeContext { schemaHref?: (name: string) => string | null; } +/** + * A property whose type is a vocabulary too wide to spell inside its table + * cell: the cell prints a sample plus a count, and these members are printed in + * full under the table (#6225). + */ +export interface RenderedProperty { + /** The type string for the table cell. */ + cell: string; + /** + * The members the cell no longer spells, for the caller to print in full — + * or `null` when the cell is complete and nothing needs relocating. + */ + allowedValues: string[] | null; +} + export const refName = (ref: string): string => ref.split('/').pop() || ref; export const isAnonymousRef = (name: string) => /^__schema\d+$/.test(name); @@ -98,6 +113,84 @@ const INLINE_KEY_LIMIT = 4; */ const INLINE_ENUM_WIDTH_LIMIT = 80; +/** + * Character budget for a vocabulary printed in ITS OWN table cell — the one + * position `INLINE_ENUM_WIDTH_LIMIT` deliberately never touches (#6225). + * + * Over it the cell prints a sample plus a count AND `formatPropertyType` hands + * the members back so `build-docs.ts` can print them in full underneath. The + * two are one decision: this budget may only be spent where the relocation + * happens, which is why it is read from `formatPropertyType` and never from + * `formatType` itself. + * + * WHY 160, and why it is NOT the 80 that governs the in-shape copy. The two + * positions have opposite costs. Eliding a copy inside a summary is nearly + * free — the full list is elsewhere, or the JSON Schema is the authority — so + * #5340 could put that budget right at the population's density collapse. + * Relocating a vocabulary out of its OWN row costs a whole `### Allowed Values` + * section on the page, so the budget wants to be as LOOSE as the width goal + * allows, not as tight as the population suggests. + * + * Measured across the 216 generated pages (8541 type cells, 893 of them a + * top-level `Enum<…>`), regenerating at each candidate and re-measuring the + * emitted `.mdx`: + * + * budget | 40 | 80 | 120 | 160 | 176 | 184 | 200 | 240 | 320 + * relocated| 227 | 72 | 41 | 25 | 24 | 24 | 17 | 14 | 9 + * cells>200| 121 | 121 | 121 | 121 | 121 | 125 | 144 | 145 | 145 + * cells>400| 18 | 18 | 18 | 18 | 18 | 18 | 18 | 18 | 18 + * p99 | 227 | 227 | 227 | 227 | 227 | 227 | 227 | 247 | 247 + * + * Every budget from 40 to 176 produces the IDENTICAL width profile — cells over + * 200 at 121, over 400 at 18, over 900 at 1, p99 227, max 1538. Inside that + * band the choice is therefore not about width at all; it is only about how + * many vocabularies get moved, and that falls from 227 sections to 24. Above + * 176 the profile degrades (184 gives back 4 cells over 200, 200 gives back 23). + * + * 160 sits near the top of the flat band: it buys the best achievable width at + * 25 relocations instead of 227, and keeps a member or two of margin below the + * 184 cliff rather than overfitting to today's exact knee. That it lands at + * exactly twice `INLINE_ENUM_WIDTH_LIMIT` is the asymmetry stated plainly — a + * vocabulary's own row is worth twice the width of a sampled second copy. + */ +const TOP_LEVEL_ENUM_WIDTH_LIMIT = 160; + +/** + * How many `anyOf` variants one cell spells before printing the count of the + * rest (#6226). + * + * A union's width is variant COUNT times variant WIDTH, so neither enum budget + * above can reach it: `ui/app.mdx`'s `App.navigation` prints + * `{ id: string; label: string; icon?: string; order?: number; … }` nine times, + * seven of them character-identical, for 582 characters of correct-but-repeated + * type. The maintainer's ruling on #6226 chose a cap on the count that prints + * how many variants it hid, over a whole-cell character budget degrading to + * `object` and over restoring `$ref` links: those lose more information, and a + * SECOND elision style in the same table is worse than the width it would fix. + * + * WHY 4. The corpus renders 353 unions, and their arity is as lopsided as the + * enum widths were: + * + * variants | 2 | 3 | 4 | 5 | 6 | 7 | 9 + * unions | 256 | 53 | 18 | 12 | 8 | 2 | 4 + * cumulative 72.5% 87.5% 92.6% 96.0% 98.3% 98.9% 100% + * + * A cap of 4 leaves 92.6% of every union in the corpus spelled out in full and + * touches only the 26 in the tail. Measured on the emitted pages at each + * candidate (enum budget held at 160): cap 2 → 8 cells over 400, cap 3 → 8, + * cap 4 → 9, cap 5 → 11, cap 6 → 18, i.e. no better than no cap at all. So 4 is + * the loosest cap that still does essentially all of the available work: going + * to 3 recovers ONE more wide cell while eliding 44 unions instead of 26. + * + * It is also the cap the reader already meets one line up. `INLINE_KEY_LIMIT` + * is 4, so a cell shows at most four declared KEYS of an object; showing at + * most four VARIANTS of a union is the same reading budget on the other axis, + * and #6226's own issue body proposed the cap that way. Two different "how many + * before `…`" numbers inside one cell would reintroduce, between the two + * elisions, exactly the inconsistency the ruling picked this option to avoid. + */ +const VARIANT_LIMIT = 4; + /** * Does this rendered type carry a top-level `&` or `|`, i.e. would suffixing * `[]` re-associate it? @@ -202,8 +295,8 @@ function formatLiteral(value: unknown): string { } /** - * An `enum` node's members, joined — and, inside a shape summary only, cut to - * `INLINE_ENUM_WIDTH_LIMIT` with an explicit count of what was cut. + * An `enum` node's members, joined — and, when a budget is given, cut to it with + * an explicit count of what was cut. * * The elided spelling is `Enum<'text' | 'textarea' | … +42 more>`-shaped: * `…` is the same "there is more" token the key elision above already uses, and @@ -213,26 +306,29 @@ function formatLiteral(value: unknown): string { * lies by omission is worse than a wide one. With the count, the cell states * exactly what it is: a sample of a 49-term vocabulary. * - * Where the rest stays readable, in order of what a reader hits first: - * 1. The vocabulary's OWN row is never elided — `ctx.inShapeSummary` is only - * set below a summary's `{ … }`. For 457 of the 805 in-shape occurrences - * the identical list is printed in full elsewhere on the SAME page (the - * named schema's own `type` row, or its `### Allowed Values` bullets): - * `BulkActionDef.params` is one — `BulkActionParam.type` two sections down - * carries all 49 — and so is every `api/*.mdx` `error` shape, whose codes - * are spelled out on `ErrorResponse.code`. - * 2. For the remaining 348 the elided cell is the only place on that page, so - * the count is doing the work by itself, and the JSON Schema under - * `json-schema/` remains the authority it always was. - * The marker cannot be an anchor to (1): Zod inlines enums, so the node reaching - * this function is a bare `{ type: 'string', enum: [...] }` with no `$ref` and - * no name to link — inventing one would be guessing at which page-local heading - * happens to carry the same members. + * TWO budgets reach this function, and which one applies decides where the rest + * of the vocabulary stays readable: + * 1. `INLINE_ENUM_WIDTH_LIMIT`, below an inline summary's `{ … }` (#5340). + * That cell holds a SECOND copy. For 457 of the 805 in-shape occurrences + * the identical list is printed in full elsewhere on the same page + * (`BulkActionDef.params` is one — `BulkActionParam.type` two sections down + * carries all 49). For the remaining 348 the JSON Schema under + * `json-schema/` is the authority, as it always was. + * 2. `TOP_LEVEL_ENUM_WIDTH_LIMIT`, on a property whose own type IS the + * vocabulary (#6225), reached only through `formatPropertyType`. That cell + * is often the page's ONLY copy, so nothing is cut unless the caller prints + * the members underneath the table — which is why that budget is spent in + * the one place that hands them back, and never from `formatType`. + * The marker is a count and not an anchor in both cases: Zod inlines enums, so + * the node reaching this function is a bare `{ type: 'string', enum: [...] }` + * with no `$ref` and no name to link — inventing one would be guessing at which + * page-local heading happens to carry the same members. Case 2 does not need one: + * its list is immediately below, under a heading naming that exact property. */ -function formatEnum(values: unknown[], inShapeSummary: boolean): string { +function elideEnum(values: unknown[], budget: number | null): { body: string; hidden: number } { const members = values.map((v: unknown) => formatLiteral(v)); const full = members.join(' | '); - if (!inShapeSummary || full.length <= INLINE_ENUM_WIDTH_LIMIT) return `Enum<${full}>`; + if (budget === null || full.length <= budget) return { body: full, hidden: 0 }; // Fill greedily, never below one member: a sample of zero states nothing, and // the member widths are the schema's, not ours to bound. @@ -240,7 +336,7 @@ function formatEnum(values: unknown[], inShapeSummary: boolean): string { let width = 0; for (const member of members) { const cost = shown.length === 0 ? member.length : member.length + ' | '.length; - if (shown.length > 0 && width + cost > INLINE_ENUM_WIDTH_LIMIT) break; + if (shown.length > 0 && width + cost > budget) break; shown.push(member); width += cost; } @@ -248,20 +344,86 @@ function formatEnum(values: unknown[], inShapeSummary: boolean): string { const hidden = members.length - shown.length; // One member wider than the whole budget: it was forced in, nothing is // hidden, and `+0 more` would be a marker pointing at nothing. - if (hidden === 0) return `Enum<${full}>`; + if (hidden === 0) return { body: full, hidden: 0 }; + const elided = elideWithMarker(shown, hidden, full.length); + return elided === null ? { body: full, hidden: 0 } : { body: elided, hidden }; +} + +/** + * Join a sample with the `… +N more` marker — or refuse, returning `null`. + * + * THE MARKER MUST EARN ITS OWN FOOTPRINT. A body only a member or two over its + * budget gives back less than the marker costs to print, so eliding it trades a + * real spelling for a count and a rewritten page, and buys nothing. Measured on + * the corpus when #5340 introduced the guard: without it, 29 of 189 enum + * elisions saved fewer characters than the marker occupied (14 of them hid a + * single member and saved 2-3 characters). With it, every elision that survives + * saves at least its own cost, and a limit stops being a cliff at exactly one + * character over. + * + * Shared by all three elisions (#6225/#6226 reuse what #5340 measured): the + * enum-body budget, the top-level vocabulary relocation, and the `anyOf` + * variant cap. They differ in what they count, never in whether a marker is + * worth printing. + * + * Re-measured on the corpus with both new limits in place, by regenerating with + * this guard forced to return `elided` unconditionally: it refuses **54** of the + * 248 candidate elisions, and those 54 would together have saved **328 + * characters** — about 6 each, against a marker that costs 12-15 to print. 7 of + * the refusals are `TOP_LEVEL_ENUM_WIDTH_LIMIT` candidates, i.e. seven whole + * `### Allowed Values` sections that would have been added to a page to shave + * single digits off one cell. The guard matters MORE for the relocation than it + * did for #5340's in-shape elision, because a refusal there saved only a marker + * while a refusal here saves a page section too. + */ +function elideWithMarker(shown: string[], hidden: number, fullLength: number): string | null { + if (hidden <= 0) return null; const marker = `… +${hidden} more`; const elided = [...shown, marker].join(' | '); - // THE MARKER MUST EARN ITS OWN FOOTPRINT. A body only a member or two over - // the budget gives back less than the marker costs to print, so eliding it - // trades a real spelling for a count and a rewritten page, and buys nothing. - // Measured on the corpus: without this guard 29 of 189 elisions save fewer - // characters than the marker occupies (14 of them hide a single member and - // save 2-3 characters). With it, every elision that survives saves at least - // its own cost, and the limit stops being a cliff at exactly 81 characters. - const markerFootprint = marker.length + ' | '.length; - const worthIt = full.length - elided.length >= markerFootprint; - return `Enum<${worthIt ? elided : full}>`; + return fullLength - elided.length >= marker.length + ' | '.length ? elided : null; +} + +function formatEnum(values: unknown[], budget: number | null): string { + return `Enum<${elideEnum(values, budget).body}>`; +} + +/** + * One property's table cell — and, when its type IS a vocabulary too wide to + * spell there, the members to print in full underneath it (#6225). + * + * `build-docs.ts` has always had a rendering built for a long vocabulary — a + * `### Allowed Values` heading and one bullet per member — but it fired only + * when the WHOLE SCHEMA was `type: 'string'` + `enum`. A schema Zod hoisted into + * its own name (`data/FieldType`) got the bullets; the identical 49 members + * inlined onto a PROPERTY (`Field.type`) got a 561-character table cell, and the + * 261-member error vocabulary on `ApiError.code` got 6092. Which rendering a + * vocabulary received depended on whether it had been hoisted — a fact about + * Zod, not about how a reader needs to read it. + * + * This is the mirror of that branch, matched to it CONDITION FOR CONDITION + * (`type === 'string'` and an `enum` array): the property's own type node is + * itself the vocabulary. That exactness is what makes the relocation safe. The + * elision is applied to the SAME node whose members are handed back, so the + * cell can never be cut without the full list being printed — the two verdicts + * come from one `elideEnum` call and cannot drift apart. + * + * Deliberately NOT matched, though each renders an `Enum<…>` somewhere in its + * cell: `Enum<…>[]`, `Record>`, and a union variant + * (`Enum<…> | string`, `ui/page.mdx`'s `PageComponent.type`). For those, "the + * allowed values of this property" is not the whole truth — the members are the + * element/value/one-variant vocabulary — so a bullet list under the table would + * state something the schema does not. They keep their full spelling, exactly + * as #5340 left them. + */ +export function formatPropertyType(prop: any, ctx?: TypeContext): RenderedProperty { + if (prop && prop.type === 'string' && Array.isArray(prop.enum)) { + const { body, hidden } = elideEnum(prop.enum, TOP_LEVEL_ENUM_WIDTH_LIMIT); + if (hidden > 0) { + return { cell: `Enum<${body}>`, allowedValues: prop.enum.map((v: unknown) => String(v)) }; + } + } + return { cell: formatType(prop, ctx), allowedValues: null }; } export function formatType(prop: any, ctx?: TypeContext): string { @@ -306,7 +468,7 @@ export function formatType(prop: any, ctx?: TypeContext): string { } if (prop.enum) { - return formatEnum(prop.enum, ctx?.inShapeSummary === true); + return formatEnum(prop.enum, ctx?.inShapeSummary === true ? INLINE_ENUM_WIDTH_LIMIT : null); } if (prop.const !== undefined) { @@ -315,7 +477,15 @@ export function formatType(prop: any, ctx?: TypeContext): string { if (prop.anyOf || prop.oneOf) { const variants = prop.anyOf || prop.oneOf; - return variants.map((v: any) => formatType(v, ctx)).join(' | '); + const rendered = variants.map((v: any) => formatType(v, ctx)); + const full = rendered.join(' | '); + if (rendered.length <= VARIANT_LIMIT) return full; + // The variants a reader does not see are counted, never silently dropped — + // the principle #5340 established for enum members, applied to the other + // axis a cell grows along (#6226). `elideWithMarker` keeps the count from + // costing more than the spellings it replaces. + const elided = elideWithMarker(rendered.slice(0, VARIANT_LIMIT), rendered.length - VARIANT_LIMIT, full.length); + return elided ?? full; } if (prop.type === 'object') {