diff --git a/.changeset/remove-dead-metadata-props-2377.md b/.changeset/remove-dead-metadata-props-2377.md new file mode 100644 index 0000000000..24fb4240ce --- /dev/null +++ b/.changeset/remove-dead-metadata-props-2377.md @@ -0,0 +1,55 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec)!: remove dead author-facing metadata properties (#2377, ADR-0049 enforce-or-remove) + +Breaking spec-surface removal, versioned as `minor` per the launch-window changeset +policy (a `major` would promote the whole fixed-group monorepo; breaking cleanups ride +the minor line, as with #2402 → 11.1.0). + +Removes a batch of spec properties that parsed but had **no runtime consumer** — +authoring them was a false affordance (especially dangerous for AI-authored +metadata). Verified dead against the liveness ledger (`packages/spec/liveness/*.json`) +and a repo-wide grep of readers. This is the follow-up slice to #2402. + +## Removed (each was `dead` + no reader anywhere) + +- **field** (`field.zod.ts`): `vectorConfig` (+ `VectorConfigSchema` + types), + `fileAttachmentConfig` (+ `FileAttachmentConfigSchema` + types), `dependencies`. + Vector fields keep the live flat `dimensions` prop; file/image fields keep the + live flat `multiple`/`accept`/`maxSize` siblings. +- **object** (`object.zod.ts`): `versioning` (+ `VersioningConfigSchema`), + `softDelete` (+ `SoftDeleteConfigSchema`), `search` (+ `SearchConfigSchema`), + `recordName`, `keyPrefix`. Each is now a **rejecting tombstone** in + `UNKNOWN_KEY_GUIDANCE` carrying the upgrade prescription. +- **action** (`action.zod.ts`): `timeout` (server uses `body.timeoutMs`; no + action-level timeout is enforced). +- **agent** (`agent.zod.ts`): `planning.strategy`, `planning.allowReplan` + (only `planning.maxIterations` is read by the runtime). +- **dataset** (`dataset.zod.ts`): `measures.certified` (declared-but-unenforced + governance flag — never compiled into the Cube). + +Liveness ledgers, the ledger README table, and `api-surface.json` are updated; +the removed sub-schema keys are dropped from `json-schema.manifest.json`. + +## Migration + +- **field/agent/dataset/action props**: authoring them is now silently stripped + (they never did anything). Remove them. Vector → set flat `dimensions`; + file/image → set flat `multiple`/`accept`/`maxSize`. +- **object props**: `ObjectSchema.create()` now throws a located error naming the + replacement — `versioning`/`softDelete` → hard deletes + `Field.trackHistory` / + `lifecycle`; `search` → `searchableFields`; `recordName` → an `autonumber` + `Field` designated as `nameField`; `keyPrefix` → remove (never had an effect). + +## Deliberately NOT removed (dead, but entangled — a scoped follow-up) + +`field.index`/`columnName`/`referenceFilters` and object +`tags`/`active`/`isSystem`/`abstract`/`enable.searchable`/`enable.trash`/`enable.mru` +and `agent.tenantId` are surfaced in the Studio metadata-authoring forms +(`*.form.ts`) — removing them cascades into i18n bundle regeneration, so they are +deferred. `action.type:'form'` has a dedicated build-time lint (`lint-view-refs.ts`) +and a first-party showcase usage, so it needs a UX decision. `field.columnName` +additionally has an ADR-0062 D7 lint. These stay `dead` + `authorWarn` in the +ledgers. diff --git a/content/docs/references/ai/agent.mdx b/content/docs/references/ai/agent.mdx index bd63714351..afd9dbf5b9 100644 --- a/content/docs/references/ai/agent.mdx +++ b/content/docs/references/ai/agent.mdx @@ -85,7 +85,7 @@ const result = AIKnowledge.parse(data); | **permissions** | `string[]` | optional | Required permission-set capabilities | | **tenantId** | `string` | optional | Tenant/Organization ID | | **visibility** | `Enum<'global' \| 'organization' \| 'private'>` | ✅ | [EXPERIMENTAL — NOT ENFORCED, #1901] Intended listing scope. No runtime consumer yet; use access/permissions for real gating. | -| **planning** | `{ strategy: Enum<'react' \| 'plan_and_execute' \| 'reflexion' \| 'tree_of_thought'>; maxIterations: integer; allowReplan: boolean }` | optional | Autonomous reasoning and planning configuration | +| **planning** | `{ maxIterations: integer }` | optional | Autonomous reasoning and planning configuration | | **memory** | `{ longTerm?: object; reflectionInterval?: integer }` | optional | Agent memory management | | **guardrails** | `{ maxTokensPerInvocation?: integer; maxExecutionTimeSec?: integer; blockedTopics?: string[] }` | optional | Safety guardrails for the agent | | **structuredOutput** | `{ format: Enum<'json_object' \| 'json_schema' \| 'regex' \| 'grammar' \| 'xml'>; schema?: Record; strict: boolean; retryOnValidationFailure: boolean; … }` | optional | Structured output format and validation configuration | diff --git a/content/docs/references/api/meta.json b/content/docs/references/api/meta.json index eefad9f2c8..dbb5734789 100644 --- a/content/docs/references/api/meta.json +++ b/content/docs/references/api/meta.json @@ -24,7 +24,6 @@ "metadata", "metadata-plugin", "notification", - "object", "odata", "package-api", "package-registry", diff --git a/content/docs/references/api/object.mdx b/content/docs/references/api/object.mdx deleted file mode 100644 index cbdb9b5208..0000000000 --- a/content/docs/references/api/object.mdx +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Object -description: Object protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - - -**Source:** `packages/spec/src/api/object.zod.ts` - - -## TypeScript Usage - -```typescript -import { VersioningConfig } from '@objectstack/spec/api'; -import type { VersioningConfig } from '@objectstack/spec/api'; - -// Validate data -const result = VersioningConfig.parse(data); -``` - ---- - -## VersioningConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **strategy** | `Enum<'urlPath' \| 'header' \| 'queryParam' \| 'dateBased'>` | ✅ | How the API version is specified by clients | -| **current** | `string` | ✅ | The current/recommended API version identifier | -| **default** | `string` | ✅ | Fallback version when client does not specify one | -| **versions** | `{ version: string; status: Enum<'preview' \| 'current' \| 'supported' \| 'deprecated' \| 'retired'>; releasedAt: string; deprecatedAt?: string; … }[]` | ✅ | All available API versions with lifecycle metadata | -| **headerName** | `string` | ✅ | HTTP header name for version negotiation (header/dateBased strategies) | -| **queryParamName** | `string` | ✅ | Query parameter name for version specification (queryParam strategy) | -| **urlPrefix** | `string` | ✅ | URL prefix before version segment (urlPath strategy) | -| **deprecation** | `{ warnHeader: boolean; sunsetHeader: boolean; linkHeader: boolean; rejectRetired: boolean; … }` | optional | Deprecation lifecycle behavior | -| **includeInDiscovery** | `boolean` | ✅ | Include version information in the API discovery endpoint | - - ---- - diff --git a/content/docs/references/api/versioning.mdx b/content/docs/references/api/versioning.mdx index d0ecfcc888..f9f0b288f1 100644 --- a/content/docs/references/api/versioning.mdx +++ b/content/docs/references/api/versioning.mdx @@ -30,8 +30,8 @@ Architecture Alignment: ## TypeScript Usage ```typescript -import { VersionDefinition, VersionNegotiationResponse, VersionStatus, VersioningStrategy } from '@objectstack/spec/api'; -import type { VersionDefinition, VersionNegotiationResponse, VersionStatus, VersioningStrategy } from '@objectstack/spec/api'; +import { VersionDefinition, VersionNegotiationResponse, VersionStatus, VersioningConfig, VersioningStrategy } from '@objectstack/spec/api'; +import type { VersionDefinition, VersionNegotiationResponse, VersionStatus, VersioningConfig, VersioningStrategy } from '@objectstack/spec/api'; // Validate data const result = VersionDefinition.parse(data); @@ -84,6 +84,25 @@ const result = VersionDefinition.parse(data); * `retired` +--- + +## VersioningConfig + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **strategy** | `Enum<'urlPath' \| 'header' \| 'queryParam' \| 'dateBased'>` | ✅ | How the API version is specified by clients | +| **current** | `string` | ✅ | The current/recommended API version identifier | +| **default** | `string` | ✅ | Fallback version when client does not specify one | +| **versions** | `{ version: string; status: Enum<'preview' \| 'current' \| 'supported' \| 'deprecated' \| 'retired'>; releasedAt: string; deprecatedAt?: string; … }[]` | ✅ | All available API versions with lifecycle metadata | +| **headerName** | `string` | ✅ | HTTP header name for version negotiation (header/dateBased strategies) | +| **queryParamName** | `string` | ✅ | Query parameter name for version specification (queryParam strategy) | +| **urlPrefix** | `string` | ✅ | URL prefix before version segment (urlPath strategy) | +| **deprecation** | `{ warnHeader: boolean; sunsetHeader: boolean; linkHeader: boolean; rejectRetired: boolean; … }` | optional | Deprecation lifecycle behavior | +| **includeInDiscovery** | `boolean` | ✅ | Include version information in the API discovery endpoint | + + --- ## VersioningStrategy diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 3253e89243..8bfb0f40b4 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -14,8 +14,8 @@ Field Type Enum ## TypeScript Usage ```typescript -import { Address, ComputedFieldCache, CurrencyConfig, CurrencyValue, DataQualityRules, Field, FieldType, FileAttachmentConfig, LocationCoordinates, SelectOption, VectorConfig } from '@objectstack/spec/data'; -import type { Address, ComputedFieldCache, CurrencyConfig, CurrencyValue, DataQualityRules, Field, FieldType, FileAttachmentConfig, LocationCoordinates, SelectOption, VectorConfig } from '@objectstack/spec/data'; +import { Address, ComputedFieldCache, CurrencyConfig, CurrencyValue, DataQualityRules, Field, FieldType, LocationCoordinates, SelectOption } from '@objectstack/spec/data'; +import type { Address, ComputedFieldCache, CurrencyConfig, CurrencyValue, DataQualityRules, Field, FieldType, LocationCoordinates, SelectOption } from '@objectstack/spec/data'; // Validate data const result = Address.parse(data); @@ -139,10 +139,7 @@ const result = Address.parse(data); | **step** | `number` | optional | Step increment for slider (default: 1) | | **currencyConfig** | `{ precision?: integer; currencyMode?: Enum<'dynamic' \| 'fixed'>; defaultCurrency?: string }` | optional | Configuration for currency field type | | **dimensions** | `integer` | optional | Vector dimensionality (e.g., 1536 for OpenAI embeddings) | -| **vectorConfig** | `{ dimensions: integer; distanceMetric?: Enum<'cosine' \| 'euclidean' \| 'dotProduct' \| 'manhattan'>; normalized?: boolean; indexed?: boolean; … }` | optional | Configuration for vector field type (AI/ML embeddings) | -| **fileAttachmentConfig** | `{ minSize?: number; maxSize?: number; allowedTypes?: string[]; blockedTypes?: string[]; … }` | optional | Configuration for file and attachment field types | | **trackHistory** | `boolean` | optional | Render this field's value changes as human-readable entries on the record activity timeline (ADR-0052 §5b). Opt-in per field. | -| **dependencies** | `string[]` | optional | Array of field names that this field depends on (for formulas, visibility rules, etc.) | | **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") | | **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is shown only when TRUE (else hidden). e.g. P`record.type == 'invoice'` | | **readonlyWhen** | `string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is read-only when TRUE. e.g. P`record.status == 'paid'` | @@ -217,40 +214,6 @@ const result = Address.parse(data); * `vector` ---- - -## FileAttachmentConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **minSize** | `number` | optional | Minimum file size in bytes | -| **maxSize** | `number` | optional | Maximum file size in bytes (e.g., 10485760 = 10MB) | -| **allowedTypes** | `string[]` | optional | Allowed file extensions (e.g., [".pdf", ".docx", ".jpg"]) | -| **blockedTypes** | `string[]` | optional | Blocked file extensions (e.g., [".exe", ".bat", ".sh"]) | -| **allowedMimeTypes** | `string[]` | optional | Allowed MIME types (e.g., ["image/jpeg", "application/pdf"]) | -| **blockedMimeTypes** | `string[]` | optional | Blocked MIME types | -| **virusScan** | `boolean` | ✅ | Enable virus scanning for uploaded files | -| **virusScanProvider** | `Enum<'clamav' \| 'virustotal' \| 'metadefender' \| 'custom'>` | optional | Virus scanning service provider | -| **virusScanOnUpload** | `boolean` | ✅ | Scan files immediately on upload | -| **quarantineOnThreat** | `boolean` | ✅ | Quarantine files if threat detected | -| **storageProvider** | `string` | optional | Object storage provider name (references ObjectStorageConfig) | -| **storageBucket** | `string` | optional | Target bucket name | -| **storagePrefix** | `string` | optional | Storage path prefix (e.g., "uploads/documents/") | -| **imageValidation** | `{ minWidth?: number; maxWidth?: number; minHeight?: number; maxHeight?: number; … }` | optional | Image-specific validation rules | -| **allowMultiple** | `boolean` | ✅ | Allow multiple file uploads (overrides field.multiple) | -| **allowReplace** | `boolean` | ✅ | Allow replacing existing files | -| **allowDelete** | `boolean` | ✅ | Allow deleting uploaded files | -| **requireUpload** | `boolean` | ✅ | Require at least one file when field is required | -| **extractMetadata** | `boolean` | ✅ | Extract file metadata (name, size, type, etc.) | -| **extractText** | `boolean` | ✅ | Extract text content from documents (OCR/parsing) | -| **versioningEnabled** | `boolean` | ✅ | Keep previous versions of replaced files | -| **maxVersions** | `number` | optional | Maximum number of versions to retain | -| **publicRead** | `boolean` | ✅ | Allow public read access to uploaded files | -| **presignedUrlExpiry** | `number` | ✅ | Presigned URL expiration in seconds (default: 1 hour) | - - --- ## LocationCoordinates @@ -282,18 +245,3 @@ const result = Address.parse(data); --- -## VectorConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **dimensions** | `integer` | ✅ | Vector dimensionality (e.g., 1536 for OpenAI embeddings) | -| **distanceMetric** | `Enum<'cosine' \| 'euclidean' \| 'dotProduct' \| 'manhattan'>` | ✅ | Distance/similarity metric for vector search | -| **normalized** | `boolean` | ✅ | Whether vectors are normalized (unit length) | -| **indexed** | `boolean` | ✅ | Whether to create a vector index for fast similarity search | -| **indexType** | `Enum<'hnsw' \| 'ivfflat' \| 'flat'>` | optional | Vector index algorithm (HNSW for high accuracy, IVFFlat for large datasets) | - - ---- - diff --git a/content/docs/references/data/meta.json b/content/docs/references/data/meta.json index e6a34a191d..74fff27af6 100644 --- a/content/docs/references/data/meta.json +++ b/content/docs/references/data/meta.json @@ -20,7 +20,6 @@ "notification", "object", "query", - "search-engine", "seed", "seed-loader", "subscription", diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 25fbc6161d..47e225a0d2 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -14,8 +14,8 @@ API Operations Enum ## TypeScript Usage ```typescript -import { ApiMethod, Index, Lifecycle, LifecycleClass, Object, ObjectAccessConfig, ObjectCapabilities, ObjectExtension, ObjectExternalBinding, ObjectFieldGroup, ObjectOwnershipEnum, ObjectRequiredPermissions, PerOperationRequiredPermissions, RowCrudActionOverride, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data'; -import type { ApiMethod, Index, Lifecycle, LifecycleClass, Object, ObjectAccessConfig, ObjectCapabilities, ObjectExtension, ObjectExternalBinding, ObjectFieldGroup, ObjectOwnershipEnum, ObjectRequiredPermissions, PerOperationRequiredPermissions, RowCrudActionOverride, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data'; +import { ApiMethod, Index, Lifecycle, LifecycleClass, Object, ObjectAccessConfig, ObjectCapabilities, ObjectExtension, ObjectExternalBinding, ObjectFieldGroup, ObjectOwnershipEnum, ObjectRequiredPermissions, PerOperationRequiredPermissions, RowCrudActionOverride, TenancyConfig } from '@objectstack/spec/data'; +import type { ApiMethod, Index, Lifecycle, LifecycleClass, Object, ObjectAccessConfig, ObjectCapabilities, ObjectExtension, ObjectExternalBinding, ObjectFieldGroup, ObjectOwnershipEnum, ObjectRequiredPermissions, PerOperationRequiredPermissions, RowCrudActionOverride, TenancyConfig } from '@objectstack/spec/data'; // Validate data const result = ApiMethod.parse(data); @@ -115,25 +115,20 @@ const result = ApiMethod.parse(data); | **tenancy** | `{ enabled: boolean; tenantField?: string }` | optional | Multi-tenancy configuration for SaaS applications | | **access** | `{ default?: Enum<'public' \| 'private'> }` | optional | [ADR-0066 D2] Object exposure posture (public-by-default vs private secure-by-default). | | **requiredPermissions** | `string[] \| { read?: string[]; create?: string[]; update?: string[]; delete?: string[] }` | optional | [ADR-0066 D3/⑤] Capabilities required to access this object (AND-gate) — `string[]` gates all CRUD, or a `{read,create,update,delete}` map gates per operation. | -| **softDelete** | `{ enabled: boolean; field?: string; cascadeDelete?: boolean }` | optional | Soft delete (trash/recycle bin) configuration | -| **versioning** | `{ enabled: boolean; strategy: Enum<'snapshot' \| 'delta' \| 'event-sourcing'>; retentionDays?: number; versionField?: string }` | optional | Record versioning and history tracking configuration | | **lifecycle** | `{ class: Enum<'record' \| 'audit' \| 'telemetry' \| 'transient' \| 'event'>; retention?: object; ttl?: object; storage?: object; … }` | optional | Data lifecycle contract (ADR-0057): class + retention/ttl/rotation/archive policies enforced by the platform LifecycleService. | | **validations** | `any[]` | optional | Object-level validation rules | | **activityMilestones** | `{ field: string; value: string; summary: string; type?: string }[]` | optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). | -| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). Pairs with recordName. | +| **nameField** | `string` | optional | [ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). | | **displayNameField** | `string` | optional | [DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. | -| **recordName** | `{ type: Enum<'text' \| 'autonumber'>; displayFormat?: string; startNumber?: integer }` | optional | Record name generation configuration (Salesforce pattern) | | **titleFormat** | `string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. | | **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. | | **stageField** | `string \| 'false'` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. | | **listViews** | `Record; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) | | **searchableFields** | `string[]` | optional | Fields the `$search` query matches against (ADR-0061). Canonical default for the record picker, list quick-search and global search; views may narrow it. When unset, search auto-defaults to the name/title field plus short-text fields. | -| **search** | `{ fields: string[]; displayFields?: string[]; filters?: string[] }` | optional | Search engine configuration | | **enable** | `{ trackHistory?: boolean; searchable?: boolean; apiEnabled?: boolean; apiMethods?: Enum<'get' \| 'list' \| 'create' \| 'update' \| 'delete' \| 'upsert' \| 'bulk' \| 'aggregate' \| 'history' \| 'search' \| 'restore' \| 'purge' \| 'import' \| 'export'>[]; … }` | optional | Enabled system features modules | | **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | Org-Wide Default record visibility (OWD) for INTERNAL users. Canonical four only (legacy aliases removed, ADR-0090 D4): private (owner-only) \| public_read (everyone reads, owner writes) \| public_read_write (everyone reads+writes) \| controlled_by_parent (derived from the master record). A CUSTOM object that omits this resolves to private at runtime (ADR-0090 D1). | | **externalSharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | [ADR-0090 D11] OWD for external (portal/partner) principals. Defaults to private; must be <= sharingModel in openness. | | **publicSharing** | `{ enabled?: boolean; allowedAudiences?: Enum<'public' \| 'link_only' \| 'signed_in' \| 'email'>[]; allowedPermissions?: Enum<'view' \| 'comment' \| 'edit'>[]; maxExpiryDays?: integer; … }` | optional | Public share-link policy (Notion/Figma-style link sharing) | -| **keyPrefix** | `string` | optional | Short prefix for record IDs (e.g., "001" for Account) | | **actions** | `{ name: string; label: string; objectName?: string; icon?: string; … }[]` | optional | Actions associated with this object (auto-populated from top-level actions via objectName) | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this object. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | @@ -297,19 +292,6 @@ Boolean-or-predicates override for a built-in row CRUD affordance. | **disabledWhen** | `string \| { dialect: Enum<'cel' \| 'js' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Per-record CEL predicate; true → render the row button disabled for that record. Fail-soft. | ---- - -## SoftDeleteConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | Enable soft delete (trash/recycle bin) | -| **field** | `string` | ✅ | Field name for soft delete timestamp | -| **cascadeDelete** | `boolean` | ✅ | Cascade soft delete to related records | - - --- ## TenancyConfig @@ -324,17 +306,3 @@ Boolean-or-predicates override for a built-in row CRUD affordance. --- -## VersioningConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **enabled** | `boolean` | ✅ | Enable record versioning | -| **strategy** | `Enum<'snapshot' \| 'delta' \| 'event-sourcing'>` | ✅ | Versioning strategy: snapshot (full copy), delta (changes only), event-sourcing (event log) | -| **retentionDays** | `number` | optional | Number of days to retain old versions (undefined = infinite) | -| **versionField** | `string` | ✅ | Field name for version number/timestamp | - - ---- - diff --git a/content/docs/references/data/search-engine.mdx b/content/docs/references/data/search-engine.mdx deleted file mode 100644 index 3c57784739..0000000000 --- a/content/docs/references/data/search-engine.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Search Engine -description: Search Engine protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - - -**Source:** `packages/spec/src/data/search-engine.zod.ts` - - -## TypeScript Usage - -```typescript -import { SearchConfig } from '@objectstack/spec/data'; -import type { SearchConfig } from '@objectstack/spec/data'; - -// Validate data -const result = SearchConfig.parse(data); -``` - ---- - -## SearchConfig - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **fields** | `string[]` | ✅ | Fields to index for full-text search weighting | -| **displayFields** | `string[]` | optional | Fields to display in search result cards | -| **filters** | `string[]` | optional | Default filters for search results | - - ---- - diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index 939fb22a29..d173da9537 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -98,7 +98,6 @@ const result = Action.parse(data); | **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. | | **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. | | **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. | -| **timeout** | `number` | optional | Maximum execution time in milliseconds for the action | | **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | diff --git a/content/docs/references/ui/dataset.mdx b/content/docs/references/ui/dataset.mdx index eb1ae05142..fa69258e60 100644 --- a/content/docs/references/ui/dataset.mdx +++ b/content/docs/references/ui/dataset.mdx @@ -109,7 +109,6 @@ const result = Dataset.parse(data); | **filter** | `any` | optional | | | **format** | `string` | optional | | | **currency** | `string` | optional | Display currency code (ISO 4217) | -| **certified** | `boolean` | ✅ | Blessed metric (governance checkpoint) | | **derived** | `{ op: Enum<'ratio' \| 'sum' \| 'difference' \| 'product'>; of: string[] }` | optional | | diff --git a/packages/cli/src/utils/lint-liveness-properties.test.ts b/packages/cli/src/utils/lint-liveness-properties.test.ts index 7b4f80edc7..7608378529 100644 --- a/packages/cli/src/utils/lint-liveness-properties.test.ts +++ b/packages/cli/src/utils/lint-liveness-properties.test.ts @@ -9,7 +9,7 @@ import { /** * These run against the REAL ledgers shipped by `@objectstack/spec` (the same * files the gate enforces), so they double as a contract test: if an - * `authorWarn` annotation is removed from `versioning` / `columnName` / etc., + * `authorWarn` annotation is removed from `abstract` / `columnName` / etc., * the matching assertion fails. */ @@ -18,9 +18,9 @@ const rules = (findings: { rule: string }[]) => findings.map((f) => f.rule); const paths = (findings: { message: string }[]) => findings.map((f) => f.message); describe('lintLivenessProperties', () => { - it('warns on a present dead object block (versioning)', () => { - const findings = lintLivenessProperties(objStack({ versioning: { enabled: true } })); - const v = findings.find((f) => f.message.includes('versioning')); + it('warns on a present dead object prop (abstract)', () => { + const findings = lintLivenessProperties(objStack({ abstract: true })); + const v = findings.find((f) => f.message.includes('abstract')); expect(v).toBeDefined(); expect(v!.rule).toBe(LIVENESS_DEAD_PROPERTY); expect(v!.where).toBe("object 'widget'"); @@ -71,7 +71,7 @@ describe('lintLivenessProperties', () => { it('handles objects as a keyed record (not just arrays)', () => { const findings = lintLivenessProperties({ - objects: { widget: { name: 'widget', versioning: { enabled: true } } }, + objects: { widget: { name: 'widget', abstract: true } }, }); expect(rules(findings)).toContain(LIVENESS_DEAD_PROPERTY); }); @@ -112,9 +112,9 @@ describe('lintLivenessProperties', () => { expect(hits[0].where).toBe("flow 'f1'"); }); - it('warns on action.timeout (no runtime enforcement)', () => { - const findings = lintLivenessProperties({ actions: [{ name: 'a1', timeout: 5000 }] }); - expect(paths(findings).some((m) => m.includes('`timeout`'))).toBe(true); + it('warns on action.undoable (experimental — declared but not enforced)', () => { + const findings = lintLivenessProperties({ actions: [{ name: 'a1', undoable: true }] }); + expect(paths(findings).some((m) => m.includes('`undoable`'))).toBe(true); }); it('warns on the security-shaped dead props (tool.permissions / permission.contextVariables)', () => { @@ -131,13 +131,6 @@ describe('lintLivenessProperties', () => { expect(paths(perm).some((m) => m.includes('contextVariables'))).toBe(true); }); - it('warns on dataset measure certified flag via array fan-out', () => { - const findings = lintLivenessProperties({ - datasets: [{ name: 'd1', measures: [{ name: 'arr', certified: true }] }], - }); - expect(paths(findings).some((m) => m.includes('measures.certified'))).toBe(true); - }); - it('stays silent on clean flat-collection items', () => { const findings = lintLivenessProperties({ flows: [{ name: 'clean', nodes: [{ id: 'n1' }] }], diff --git a/packages/lint/src/validate-widget-bindings.test.ts b/packages/lint/src/validate-widget-bindings.test.ts index 16949d4594..3bbaa2d935 100644 --- a/packages/lint/src/validate-widget-bindings.test.ts +++ b/packages/lint/src/validate-widget-bindings.test.ts @@ -50,7 +50,7 @@ function chartStack(widgetOverrides: Record = {}) { object: 'expense_line', dimensions: [{ name: 'category', field: 'category' }], measures: [ - { name: 'sum_amount', label: 'sum_amount', aggregate: 'sum', field: 'amount', certified: true }, + { name: 'sum_amount', label: 'sum_amount', aggregate: 'sum', field: 'amount' }, { name: 'ticket_count', label: 'ticket_count', aggregate: 'count' }, ], }], diff --git a/packages/rest/src/analytics-routes.test.ts b/packages/rest/src/analytics-routes.test.ts index 81f8a4b8ab..6e85cff6f9 100644 --- a/packages/rest/src/analytics-routes.test.ts +++ b/packages/rest/src/analytics-routes.test.ts @@ -58,9 +58,9 @@ describe('POST /analytics/dataset/query', () => { expect(res.statusCode).toBe(200); expect(res.body).toEqual({ rows: [{ region: 'NA', revenue: 100 }], fields: [] }); - // dataset was schema-validated before reaching the service (certified default applied) + // dataset was schema-validated before reaching the service const passedDataset = queryDataset.mock.calls[0][0]; - expect(passedDataset.measures[0].certified).toBe(false); + expect(passedDataset.measures[0].name).toBe('revenue'); expect(queryDataset.mock.calls[0][1]).toEqual(selection); }); diff --git a/packages/services/service-analytics/src/__tests__/dataset-compiler.test.ts b/packages/services/service-analytics/src/__tests__/dataset-compiler.test.ts index c67538280b..659fb12306 100644 --- a/packages/services/service-analytics/src/__tests__/dataset-compiler.test.ts +++ b/packages/services/service-analytics/src/__tests__/dataset-compiler.test.ts @@ -16,10 +16,10 @@ const salesDataset = DatasetSchema.parse({ { name: 'close_month', label: 'Close Month', field: 'close_date', type: 'date', dateGranularity: 'month' }, ], measures: [ - { name: 'revenue', label: 'Revenue', aggregate: 'sum', field: 'amount', certified: true, format: '$0,0.00' }, - { name: 'deal_count', label: 'Deals', aggregate: 'count', certified: false }, - { name: 'won_amount', label: 'Won', aggregate: 'sum', field: 'amount', certified: false, filter: { stage: 'won' } }, - { name: 'win_rate', label: 'Win Rate', aggregate: 'sum', certified: false, derived: { op: 'ratio', of: ['won_amount', 'revenue'] } }, + { name: 'revenue', label: 'Revenue', aggregate: 'sum', field: 'amount', format: '$0,0.00' }, + { name: 'deal_count', label: 'Deals', aggregate: 'count' }, + { name: 'won_amount', label: 'Won', aggregate: 'sum', field: 'amount', filter: { stage: 'won' } }, + { name: 'win_rate', label: 'Win Rate', aggregate: 'sum', derived: { op: 'ratio', of: ['won_amount', 'revenue'] } }, ], }); diff --git a/packages/services/service-analytics/src/__tests__/dataset-executor.test.ts b/packages/services/service-analytics/src/__tests__/dataset-executor.test.ts index d84cc6d114..625b4b6535 100644 --- a/packages/services/service-analytics/src/__tests__/dataset-executor.test.ts +++ b/packages/services/service-analytics/src/__tests__/dataset-executor.test.ts @@ -52,7 +52,7 @@ const dataset = DatasetSchema.parse({ filter: { is_deleted: { $ne: true } }, dimensions: [{ name: 'region', field: 'account.region', type: 'string' }], measures: [ - { name: 'revenue', aggregate: 'sum', field: 'amount', certified: true }, + { name: 'revenue', aggregate: 'sum', field: 'amount' }, { name: 'won_amount', aggregate: 'sum', field: 'amount', filter: { stage: 'won' } }, { name: 'win_rate', aggregate: 'sum', derived: { op: 'ratio', of: ['won_amount', 'revenue'] } }, ], diff --git a/packages/services/service-analytics/src/__tests__/dataset-rls-integration.test.ts b/packages/services/service-analytics/src/__tests__/dataset-rls-integration.test.ts index 92c9a826ec..0a5589e10a 100644 --- a/packages/services/service-analytics/src/__tests__/dataset-rls-integration.test.ts +++ b/packages/services/service-analytics/src/__tests__/dataset-rls-integration.test.ts @@ -25,7 +25,7 @@ const dataset = DatasetSchema.parse({ object: 'opportunity', include: ['account'], dimensions: [{ name: 'region', field: 'account.region', type: 'string' }], - measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', certified: true }], + measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount' }], }); /** diff --git a/packages/services/service-analytics/src/__tests__/query-dataset.test.ts b/packages/services/service-analytics/src/__tests__/query-dataset.test.ts index 402ce2bfbe..6694c6469c 100644 --- a/packages/services/service-analytics/src/__tests__/query-dataset.test.ts +++ b/packages/services/service-analytics/src/__tests__/query-dataset.test.ts @@ -11,7 +11,7 @@ const dataset = DatasetSchema.parse({ object: 'opportunity', include: ['account'], dimensions: [{ name: 'region', field: 'account.region', type: 'string' }], - measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', certified: true }], + measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount' }], }); function service(captured: { sql: string; params: unknown[] }[]) { @@ -103,7 +103,7 @@ describe('AnalyticsService.queryDataset', () => { const priced = DatasetSchema.parse({ name: 'sales_priced', label: 'Sales', object: 'opportunity', include: [], dimensions: [{ name: 'stage', field: 'stage', type: 'string' }], - measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', label: 'Revenue', format: '0,0', currency: 'USD', certified: true }], + measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', label: 'Revenue', format: '0,0', currency: 'USD' }], }); const svc = new AnalyticsService({ queryCapabilities: () => ({ nativeSql: true, objectqlAggregate: false, inMemory: false }), @@ -165,7 +165,7 @@ describe('AnalyticsService.queryDataset', () => { const labeled = DatasetSchema.parse({ name: 'sales2', label: 'Sales', object: 'opportunity', include: ['account'], dimensions: [{ name: 'region', field: 'account.region', type: 'string', label: 'Region' }], - measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', label: 'Revenue', certified: true }], + measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', label: 'Revenue' }], }); const result = await service([]).queryDataset( labeled, @@ -180,7 +180,7 @@ describe('AnalyticsService.queryDataset', () => { const dated = DatasetSchema.parse({ name: 'sales3', label: 'Sales', object: 'opportunity', include: [], dimensions: [{ name: 'closed', field: 'close_date', type: 'date' }], - measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', certified: true }], + measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount' }], }); const svc = new AnalyticsService({ queryCapabilities: () => ({ nativeSql: true, objectqlAggregate: false, inMemory: false }), @@ -198,7 +198,7 @@ describe('AnalyticsService.queryDataset', () => { const byAccount = DatasetSchema.parse({ name: 'sales_acct', label: 'Sales', object: 'opportunity', include: [], dimensions: [{ name: 'account', field: 'account', type: 'lookup', label: 'Account' }], - measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', certified: true }], + measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount' }], }); const svc = new AnalyticsService({ queryCapabilities: () => ({ nativeSql: true, objectqlAggregate: false, inMemory: false }), diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 8dcf44a72a..43be5a4725 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -321,9 +321,6 @@ "FieldReferenceSchema (const)", "FieldSchema (const)", "FieldType (type)", - "FileAttachmentConfig (type)", - "FileAttachmentConfigInput (type)", - "FileAttachmentConfigSchema (const)", "Filter (type)", "FilterCondition (type)", "FilterConditionSchema (const)", @@ -460,7 +457,6 @@ "ScriptBodySchema (const)", "ScriptValidation (type)", "ScriptValidationSchema (const)", - "SearchConfigSchema (const)", "Seed (type)", "SeedIdentity (type)", "SeedIdentitySchema (const)", @@ -485,8 +481,6 @@ "SetOperatorSchema (const)", "ShardingConfig (type)", "ShardingConfigSchema (const)", - "SoftDeleteConfig (type)", - "SoftDeleteConfigSchema (const)", "SortNode (type)", "SortNodeSchema (const)", "SpecialOperatorSchema (const)", @@ -506,11 +500,6 @@ "VALID_AST_OPERATORS (const)", "ValidationRule (type)", "ValidationRuleSchema (const)", - "VectorConfig (type)", - "VectorConfigInput (type)", - "VectorConfigSchema (const)", - "VersioningConfig (type)", - "VersioningConfigSchema (const)", "WindowFunction (const)", "WindowFunctionNode (type)", "WindowFunctionNodeSchema (const)", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index aad0c068b9..e4a5fe190d 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -789,7 +789,6 @@ "data/FieldNode", "data/FieldReference", "data/FieldType", - "data/FileAttachmentConfig", "data/FilterCondition", "data/FormatValidation", "data/FullTextSearch", @@ -844,7 +843,6 @@ "data/SSLConfig", "data/ScriptBody", "data/ScriptValidation", - "data/SearchConfig", "data/Seed", "data/SeedIdentity", "data/SeedLoadResult", @@ -855,7 +853,6 @@ "data/SelectOption", "data/SetOperator", "data/ShardingConfig", - "data/SoftDeleteConfig", "data/SortNode", "data/SpecialOperator", "data/StateMachineValidation", @@ -865,8 +862,6 @@ "data/TimeUpdateInterval", "data/TransformType", "data/ValidationRule", - "data/VectorConfig", - "data/VersioningConfig", "data/WindowFunction", "data/WindowFunctionNode", "data/WindowSpec", diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 9197cfee07..c79fd06faf 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -189,17 +189,17 @@ EOF | Type | live | exp | dead | planned | Notes | |---|---|---|---|---|---| -| object | 37 | – | 12 | 1 | versioning/partitioning/cdc tier dead; ObjectCapabilities fully live post-#2707/#2727; `tenancy.strategy`/`crossTenantAccess` REMOVED post-15.0 (#2763) — tenancy block is now strict with tombstone guidance | -| field | 55 | – | 6 | – | near-healthy; dead = referenceFilters/columnName/index/vectorConfig/fileAttachmentConfig/dependencies, all authorWarn'd | +| object | 37 | – | 7 | 1 | aspirational tier (versioning/softDelete/search/recordName/keyPrefix) REMOVED in 16.0 (#2377) — tombstoned in UNKNOWN_KEY_GUIDANCE; remaining dead = tags/active/isSystem/abstract + enable.searchable/trash/mru (authoring-form-surfaced, deferred); ObjectCapabilities otherwise live post-#2707/#2727; `tenancy.strategy`/`crossTenantAccess` REMOVED post-15.0 (#2763) | +| field | 55 | – | 3 | – | near-healthy; vectorConfig/fileAttachmentConfig/dependencies REMOVED in 16.0 (#2377); remaining dead = referenceFilters/columnName/index (authoring-form-surfaced, deferred), all authorWarn'd | | flow | 27 | – | 4 | – | dead = description/template/nodes.outputSchema/errorHandling.fallbackNodeId (engine uses fault edges) | -| action | 34 | 1 | 1 | – | `disabled` went LIVE via metadata-admin authoring UI (2026-06 audit missed objectui); only `timeout` dead | +| action | 35 | 1 | 0 | – | `disabled` went LIVE via metadata-admin authoring UI (2026-06 audit missed objectui); dead `timeout` REMOVED in 16.0 (#2377) | | hook | 11 | – | 2 | – | model-healthy; only label/description dead (benign) | | permission | 32 | – | 1 | – | CRUD/FLS/RLS live; `contextVariables` dead (RLS uses current_user.* built-ins only) | | position | 4 | – | – | – | (role's ADR-0090 successor) fully live | -| agent | 14 | 5 | 1 | – | `tenantId` dead (tenancy comes from request context); autonomy tier experimental | +| agent | 14 | 5 | 1 | – | `tenantId` dead (authoring-form-surfaced, deferred); dead `planning.strategy`/`allowReplan` REMOVED in 16.0 (#2377), only `planning.maxIterations` live; autonomy tier experimental | | tool | 9 | 1 | 1 | – | `permissions` dead — tool invocation not permission-gated by it | | skill | 10 | – | – | – | fully live | -| dataset | 19 | – | 1 | – | `measures.certified` declared-but-unenforced governance flag | +| dataset | 19 | – | 0 | – | `measures.certified` (declared-but-unenforced governance flag) REMOVED in 16.0 (#2377) | | page | 16 | – | – | 1 | fully live + one planned | | view | 68 | 2 | 5 | – | list/form drilled via `children` (#2998 Track B); dead = list.{responsive,performance} + form.{data,defaultSort,aria}, all but aria authorWarn'd; exp = form.{buttons,defaults} awaiting objectui#2545; audit-era DEAD lines superseded by re-verification (submitBehavior, sharing.lockedBy, list ViewData providers, and the ADR-0021 chart shape — all live now); level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) noted on parents — one drill level only | diff --git a/packages/spec/liveness/action.json b/packages/spec/liveness/action.json index ba8dde9380..81765c0300 100644 --- a/packages/spec/liveness/action.json +++ b/packages/spec/liveness/action.json @@ -157,12 +157,6 @@ "evidence": "objectui ActionRunner.executeUrl (objectui issue #2043)", "note": "Declarative new-tab control for STATIC type:'url' targets. ActionRunner.executeUrl reads action.openIn with priority over the legacy params.newTab/external-URL heuristic; action-button/icon/menu/group + basic/elements renderers forward it. Distinct from opensInNewTab/newTabUrl (async SSO pre-open)." }, - "timeout": { - "status": "dead", - "evidence": "action-level timeout DEAD — server uses body.timeoutMs; no UI consumer", - "authorWarn": true, - "authorHint": "No action-level timeout is enforced at runtime — remove it. Per-action execution time limits are not configurable today." - }, "aria": { "status": "live", "note": "PARTIAL — honored by a few objectui renderers, not the core action buttons/menus." diff --git a/packages/spec/liveness/agent.json b/packages/spec/liveness/agent.json index 49a8d0976e..d00ef8389b 100644 --- a/packages/spec/liveness/agent.json +++ b/packages/spec/liveness/agent.json @@ -52,7 +52,7 @@ "planning": { "status": "live", "evidence": "packages/services/service-ai/src/agent-runtime.ts", - "note": "PARTIAL — only planning.maxIterations live; strategy/allowReplan DEAD." + "note": "Only planning.maxIterations remains; the dead strategy/allowReplan knobs were removed in 16.0 (#2377)." }, "access": { "status": "live", diff --git a/packages/spec/liveness/dataset.json b/packages/spec/liveness/dataset.json index c5a210d0d3..c4cda37d8f 100644 --- a/packages/spec/liveness/dataset.json +++ b/packages/spec/liveness/dataset.json @@ -98,13 +98,6 @@ "evidence": "packages/services/service-analytics/src/analytics-service.ts:531", "note": "measure-declared currency (ISO 4217) enriched onto result fields alongside label/format, so the renderer formats the amount with a locale-correct Intl symbol rather than a '$' baked into format." }, - "certified": { - "status": "dead", - "evidence": "no runtime consumer — analytics execution never reads it; not compiled into the Cube", - "note": "measure trust/governance flag, declared but unenforced (ADR-0049 enforce-or-remove candidate). Not authorWarn'd: it may surface as an objectui badge, so it is not necessarily misleading.", - "authorWarn": true, - "authorHint": "Declared but unenforced — analytics execution never reads it, so nothing badges or restricts \"certified\" measures. Do not present governance guarantees based on this flag." - }, "derived": { "status": "live", "evidence": "packages/services/service-analytics/src/dataset-executor.ts:247", diff --git a/packages/spec/liveness/field.json b/packages/spec/liveness/field.json index 1bf7dd678f..f180962162 100644 --- a/packages/spec/liveness/field.json +++ b/packages/spec/liveness/field.json @@ -1,6 +1,6 @@ { "type": "field", - "_note": "FieldSchema (flat — all field-type configs are top-level optional props). Seeded from docs/audits/2026-06-fieldschema-property-liveness.md. ~half dead. Nested config objects (currencyConfig/vectorConfig/fileAttachmentConfig/encryptionConfig/maskingRule/cached/dataQuality) are wholly dead → classified at top level. Naming-drift props are server-live but client-snake. Framework evidence cited with paths; objectui-renderer evidence as prose.", + "_note": "FieldSchema (flat — all field-type configs are top-level optional props). Seeded from docs/audits/2026-06-fieldschema-property-liveness.md. The wholly-dead nested config objects (vectorConfig/fileAttachmentConfig/encryptionConfig/maskingRule/cached/dataQuality) were removed (#2377 + earlier pruning); currencyConfig is LIVE via the objectui renderer. Naming-drift props are server-live but client-snake. Framework evidence cited with paths; objectui-renderer evidence as prose.", "props": { "name": { "status": "live", @@ -229,25 +229,7 @@ }, "dimensions": { "status": "live", - "note": "vector field — objectui VectorField.tsx:11 reads the flat `dimensions` prop. The live authoring path (cf. dead `vectorConfig`); `Field.vector(n)` emits this." - }, - "vectorConfig": { - "status": "dead", - "evidence": "nested config — renderers read flat dimensions; no consumer, no vector-index DDL", - "authorWarn": true, - "authorHint": "This nested block is not read (no vector-index DDL). Set the flat `dimensions` sibling instead." - }, - "fileAttachmentConfig": { - "status": "dead", - "evidence": "nested config — renderers read flat multiple/accept/maxSize (FileField.tsx:16); no size/type/virus enforcement in write path", - "authorWarn": true, - "authorHint": "This nested block is not read — and note no size/type enforcement happens on write. Use the flat `multiple`/`accept`/`maxSize` siblings for the renderer." - }, - "dependencies": { - "status": "dead", - "evidence": "no consumer", - "authorWarn": true, - "authorHint": "No consumer — field dependency/cascade behavior is not implemented. Formula inputs are inferred from the expression itself; visibility rules use their own conditions." + "note": "vector field — objectui VectorField.tsx:11 reads the flat `dimensions` prop. The live authoring path; `Field.vector(n)` emits this. (The dead nested `vectorConfig` block was removed in 16.0, #2377.)" }, "inlineTitle": { "status": "live", diff --git a/packages/spec/liveness/object.json b/packages/spec/liveness/object.json index 1eff94918f..9cf26a33f8 100644 --- a/packages/spec/liveness/object.json +++ b/packages/spec/liveness/object.json @@ -187,36 +187,6 @@ } } }, - "versioning": { - "status": "dead", - "evidence": "aspirational; no runtime", - "authorWarn": true, - "authorHint": "No record-versioning engine reads this block — setting it stores nothing and snapshots no history. Remove it (use Field.trackHistory for field-level history)." - }, - "softDelete": { - "status": "dead", - "evidence": "no runtime; duplicates the (also dead) enable.trash", - "authorWarn": true, - "authorHint": "There is no soft-delete/recycle-bin runtime. Deletes are hard deletes; remove this to avoid implying restore semantics that don't exist." - }, - "search": { - "status": "dead", - "evidence": "SearchConfigSchema — no runtime; duplicates enable.searchable", - "authorWarn": true, - "authorHint": "No search-engine config is consumed. Remove it — records remain queryable via the normal data API regardless." - }, - "recordName": { - "status": "dead", - "evidence": "superseded by a field of type:'autonumber' + autonumberFormat (engine.ts:757)", - "authorWarn": true, - "authorHint": "Auto-naming is done by a `Field` of type 'autonumber' (with autonumberFormat). This block is not read — model the name field instead." - }, - "keyPrefix": { - "status": "dead", - "evidence": "no runtime", - "authorWarn": true, - "authorHint": "Record ids are not prefixed from this value (no Salesforce-style key prefix runtime). Remove it — it has no effect." - }, "tags": { "status": "dead", "evidence": "no runtime reader" diff --git a/packages/spec/src/ai/agent.test.ts b/packages/spec/src/ai/agent.test.ts index ada2df3e5d..2979fa47e0 100644 --- a/packages/spec/src/ai/agent.test.ts +++ b/packages/spec/src/ai/agent.test.ts @@ -522,30 +522,11 @@ Be precise, data-driven, and clear in your explanations.`, role: 'Strategic Planner', instructions: 'Plan and execute complex tasks.', planning: { - strategy: 'plan_and_execute', maxIterations: 20, - allowReplan: true, }, }); - expect(agent.planning?.strategy).toBe('plan_and_execute'); expect(agent.planning?.maxIterations).toBe(20); - expect(agent.planning?.allowReplan).toBe(true); - }); - - it('should accept all planning strategies', () => { - const strategies = ['react', 'plan_and_execute', 'reflexion', 'tree_of_thought'] as const; - - strategies.forEach(strategy => { - const agent = AgentSchema.parse({ - name: 'test_agent', - label: 'Test', - role: 'Test', - instructions: 'Test', - planning: { strategy }, - }); - expect(agent.planning?.strategy).toBe(strategy); - }); }); it('should apply default planning values', () => { @@ -557,9 +538,9 @@ Be precise, data-driven, and clear in your explanations.`, planning: {}, }); - expect(agent.planning?.strategy).toBe('react'); + // Only maxIterations is live; the dead strategy/allowReplan knobs were + // removed in 16.0 (#2377). expect(agent.planning?.maxIterations).toBe(10); - expect(agent.planning?.allowReplan).toBe(true); }); it('should enforce maxIterations constraints', () => { diff --git a/packages/spec/src/ai/agent.zod.ts b/packages/spec/src/ai/agent.zod.ts index 3181bba9bb..e9a7b6a25f 100644 --- a/packages/spec/src/ai/agent.zod.ts +++ b/packages/spec/src/ai/agent.zod.ts @@ -180,14 +180,8 @@ export const AgentSchema = lazySchema(() => z.object({ /** Autonomous Reasoning */ planning: z.object({ - /** Planning strategy for autonomous reasoning loops */ - strategy: z.enum(['react', 'plan_and_execute', 'reflexion', 'tree_of_thought']).default('react').describe('Autonomous reasoning strategy'), - /** Maximum reasoning iterations before stopping */ maxIterations: z.number().int().min(1).max(100).default(10).describe('Maximum planning loop iterations'), - - /** Whether the agent can revise its own plan mid-execution */ - allowReplan: z.boolean().default(true).describe('Allow dynamic re-planning based on intermediate results'), }).optional().describe('Autonomous reasoning and planning configuration'), /** Memory Management */ diff --git a/packages/spec/src/data/display-name.ts b/packages/spec/src/data/display-name.ts index a240bcfd53..e1267dfe57 100644 --- a/packages/spec/src/data/display-name.ts +++ b/packages/spec/src/data/display-name.ts @@ -20,9 +20,9 @@ * approval/notification display-enrichment path, objectql search field * resolution, and build-time lint. * - * NOTE on the schema: `nameField` is the canonical pointer (pairs with - * `recordName` — the Salesforce Name/Record-Name model). `displayNameField` is - * accepted as a DEPRECATED ALIAS at the schema level (mapped onto `nameField`). + * NOTE on the schema: `nameField` is the canonical primary-title pointer + * (ADR-0079). `displayNameField` is accepted as a DEPRECATED ALIAS at the + * schema level (mapped onto `nameField`). * Both are still read here so this resolver works against metadata that has not * yet been normalized. */ @@ -64,9 +64,9 @@ export interface DisplayNameObjectMeta { * short label (dates, numbers, booleans, media, structured/relational values, * choice tokens, and system/auto values). `autonumber` is excluded from * *derivation* here: an autonumber is a valid primary only when an author - * points at it explicitly (it pairs with `recordName.type='autonumber'`), not - * something we silently pick. `formula` is conditionally eligible — see - * `isTitleEligible`. + * points at it explicitly (an `autonumber` `Field` designated as the + * `nameField`), not something we silently pick. `formula` is conditionally + * eligible — see `isTitleEligible`. * * `phone` is deliberately excluded (a phone number is not a title); `email` IS * eligible (commonly the human handle on identity-ish objects). diff --git a/packages/spec/src/data/field.test.ts b/packages/spec/src/data/field.test.ts index 5e35a1b4db..65621f6ab2 100644 --- a/packages/spec/src/data/field.test.ts +++ b/packages/spec/src/data/field.test.ts @@ -5,14 +5,10 @@ import { SelectOptionSchema, CurrencyConfigSchema, CurrencyValueSchema, - VectorConfigSchema, - FileAttachmentConfigSchema, Field, type SelectOption, type CurrencyConfig, type CurrencyValue, - type VectorConfig, - type FileAttachmentConfig, } from './field.zod'; describe('FieldType', () => { @@ -891,533 +887,6 @@ describe('Field Factory Helpers', () => { expect((vectorField as Record).vectorConfig).toBeUndefined(); }); - it('should create vector field with custom config', () => { - const vectorField = Field.vector(512, { - name: 'image_embedding', - label: 'Image Embedding', - vectorConfig: { - dimensions: 512, - distanceMetric: 'euclidean', - normalized: true, - indexed: true, - indexType: 'hnsw', - }, - }); - - expect(vectorField.type).toBe('vector'); - expect(vectorField.vectorConfig?.dimensions).toBe(512); - expect(vectorField.vectorConfig?.distanceMetric).toBe('euclidean'); - expect(vectorField.vectorConfig?.normalized).toBe(true); - expect(vectorField.vectorConfig?.indexType).toBe('hnsw'); - }); - - it('should validate vector field with valid config', () => { - const validField = { - name: 'product_embedding', - label: 'Product Embedding', - type: 'vector' as const, - vectorConfig: { - dimensions: 768, - distanceMetric: 'dotProduct' as const, - normalized: false, - indexed: true, - indexType: 'ivfflat' as const, - }, - }; - - const result = FieldSchema.safeParse(validField); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.vectorConfig?.dimensions).toBe(768); - expect(result.data.vectorConfig?.distanceMetric).toBe('dotProduct'); - expect(result.data.vectorConfig?.indexType).toBe('ivfflat'); - } - }); - - it('should accept all distance metrics', () => { - const metrics = ['cosine', 'euclidean', 'dotProduct', 'manhattan'] as const; - - metrics.forEach(metric => { - const config = { - dimensions: 100, - distanceMetric: metric, - normalized: false, - indexed: true, - }; - - expect(() => VectorConfigSchema.parse(config)).not.toThrow(); - }); - }); - - it('should accept all index types', () => { - const indexTypes = ['hnsw', 'ivfflat', 'flat'] as const; - - indexTypes.forEach(indexType => { - const config = { - dimensions: 100, - distanceMetric: 'cosine' as const, - normalized: false, - indexed: true, - indexType, - }; - - expect(() => VectorConfigSchema.parse(config)).not.toThrow(); - }); - }); - - it('should apply default values for vector config', () => { - const config = VectorConfigSchema.parse({ - dimensions: 1536, - }); - - expect(config.dimensions).toBe(1536); - expect(config.distanceMetric).toBe('cosine'); - expect(config.normalized).toBe(false); - expect(config.indexed).toBe(true); - }); - - it('should reject invalid dimension values', () => { - const invalidDimensions = [0, -1, 10001, 1.5]; - - invalidDimensions.forEach(dimensions => { - expect(() => VectorConfigSchema.parse({ dimensions })).toThrow(); - }); - }); - - it('should accept valid dimension range', () => { - const validDimensions = [1, 128, 512, 768, 1536, 3072, 10000]; - - validDimensions.forEach(dimensions => { - expect(() => VectorConfigSchema.parse({ dimensions })).not.toThrow(); - }); - }); - - it('should work with OpenAI embeddings', () => { - const openAIField = Field.vector(1536, { - name: 'text_embedding', - label: 'Text Embedding (OpenAI)', - description: 'OpenAI text-embedding-ada-002', - vectorConfig: { - dimensions: 1536, - distanceMetric: 'cosine', - normalized: true, - indexed: true, - indexType: 'hnsw', - }, - }); - - expect(openAIField.type).toBe('vector'); - expect(openAIField.vectorConfig?.dimensions).toBe(1536); - expect(openAIField.description).toBe('OpenAI text-embedding-ada-002'); - }); - - it('should work with image embeddings', () => { - const imageEmbeddingField = Field.vector(512, { - name: 'image_features', - label: 'Image Features (ResNet-50)', - vectorConfig: { - dimensions: 512, - distanceMetric: 'euclidean', - normalized: true, - indexed: true, - }, - }); - - expect(imageEmbeddingField.type).toBe('vector'); - expect(imageEmbeddingField.vectorConfig?.dimensions).toBe(512); - expect(imageEmbeddingField.vectorConfig?.distanceMetric).toBe('euclidean'); - }); - - it('should support RAG use case', () => { - const ragField = Field.vector(768, { - name: 'document_embedding', - label: 'Document Embedding', - description: 'Semantic embedding for RAG retrieval', - required: false, - searchable: true, - vectorConfig: { - dimensions: 768, - distanceMetric: 'cosine', - normalized: true, - indexed: true, - indexType: 'hnsw', - }, - }); - - expect(ragField.type).toBe('vector'); - expect(ragField.searchable).toBe(true); - expect(ragField.vectorConfig?.indexed).toBe(true); - expect(ragField.vectorConfig?.indexType).toBe('hnsw'); - }); - }); - - describe('FileAttachmentConfigSchema', () => { - it('should accept minimal config', () => { - const config = FileAttachmentConfigSchema.parse({}); - - expect(config.virusScan).toBe(false); - expect(config.virusScanOnUpload).toBe(true); - expect(config.quarantineOnThreat).toBe(true); - expect(config.allowMultiple).toBe(false); - expect(config.allowReplace).toBe(true); - expect(config.allowDelete).toBe(true); - expect(config.requireUpload).toBe(false); - expect(config.extractMetadata).toBe(true); - expect(config.extractText).toBe(false); - expect(config.versioningEnabled).toBe(false); - expect(config.publicRead).toBe(false); - expect(config.presignedUrlExpiry).toBe(3600); - }); - - it('should accept file size limits', () => { - const config = FileAttachmentConfigSchema.parse({ - minSize: 1024, - maxSize: 10485760, // 10MB - }); - - expect(config.minSize).toBe(1024); - expect(config.maxSize).toBe(10485760); - }); - - it('should accept allowed file types', () => { - const config = FileAttachmentConfigSchema.parse({ - allowedTypes: ['.pdf', '.docx', '.xlsx'], - allowedMimeTypes: ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'], - }); - - expect(config.allowedTypes).toHaveLength(3); - expect(config.allowedMimeTypes).toHaveLength(2); - }); - - it('should accept blocked file types', () => { - const config = FileAttachmentConfigSchema.parse({ - blockedTypes: ['.exe', '.bat', '.sh'], - blockedMimeTypes: ['application/x-executable'], - }); - - expect(config.blockedTypes).toHaveLength(3); - expect(config.blockedMimeTypes).toHaveLength(1); - }); - - it('should accept virus scanning configuration', () => { - const config = FileAttachmentConfigSchema.parse({ - virusScan: true, - virusScanProvider: 'clamav', - virusScanOnUpload: true, - quarantineOnThreat: true, - }); - - expect(config.virusScan).toBe(true); - expect(config.virusScanProvider).toBe('clamav'); - expect(config.virusScanOnUpload).toBe(true); - expect(config.quarantineOnThreat).toBe(true); - }); - - it('should accept all virus scan providers', () => { - const providers = ['clamav', 'virustotal', 'metadefender', 'custom'] as const; - - providers.forEach(provider => { - const config = { - virusScan: true, - virusScanProvider: provider, - }; - - expect(() => FileAttachmentConfigSchema.parse(config)).not.toThrow(); - }); - }); - - it('should accept storage configuration', () => { - const config = FileAttachmentConfigSchema.parse({ - storageProvider: 'aws_s3_storage', - storageBucket: 'user_uploads', - storagePrefix: 'documents/', - }); - - expect(config.storageProvider).toBe('aws_s3_storage'); - expect(config.storageBucket).toBe('user_uploads'); - expect(config.storagePrefix).toBe('documents/'); - }); - - it('should accept image validation config', () => { - const config = FileAttachmentConfigSchema.parse({ - imageValidation: { - minWidth: 100, - maxWidth: 4096, - minHeight: 100, - maxHeight: 4096, - aspectRatio: '16:9', - generateThumbnails: true, - thumbnailSizes: [ - { name: 'small', width: 150, height: 150, crop: true }, - { name: 'medium', width: 300, height: 300, crop: true }, - { name: 'large', width: 600, height: 600, crop: false }, - ], - preserveMetadata: false, - autoRotate: true, - }, - }); - - expect(config.imageValidation?.minWidth).toBe(100); - expect(config.imageValidation?.maxWidth).toBe(4096); - expect(config.imageValidation?.generateThumbnails).toBe(true); - expect(config.imageValidation?.thumbnailSizes).toHaveLength(3); - }); - - it('should accept upload behavior config', () => { - const config = FileAttachmentConfigSchema.parse({ - allowMultiple: true, - allowReplace: false, - allowDelete: false, - requireUpload: true, - }); - - expect(config.allowMultiple).toBe(true); - expect(config.allowReplace).toBe(false); - expect(config.allowDelete).toBe(false); - expect(config.requireUpload).toBe(true); - }); - - it('should accept metadata extraction config', () => { - const config = FileAttachmentConfigSchema.parse({ - extractMetadata: true, - extractText: true, - }); - - expect(config.extractMetadata).toBe(true); - expect(config.extractText).toBe(true); - }); - - it('should accept versioning config', () => { - const config = FileAttachmentConfigSchema.parse({ - versioningEnabled: true, - maxVersions: 10, - }); - - expect(config.versioningEnabled).toBe(true); - expect(config.maxVersions).toBe(10); - }); - - it('should accept access control config', () => { - const config = FileAttachmentConfigSchema.parse({ - publicRead: true, - presignedUrlExpiry: 1800, - }); - - expect(config.publicRead).toBe(true); - expect(config.presignedUrlExpiry).toBe(1800); - }); - - it('should enforce presigned URL expiry limits', () => { - // Min 60 seconds - expect(() => FileAttachmentConfigSchema.parse({ - presignedUrlExpiry: 59, - })).toThrow(); - - expect(() => FileAttachmentConfigSchema.parse({ - presignedUrlExpiry: 60, - })).not.toThrow(); - - // Max 7 days - expect(() => FileAttachmentConfigSchema.parse({ - presignedUrlExpiry: 604800, - })).not.toThrow(); - - expect(() => FileAttachmentConfigSchema.parse({ - presignedUrlExpiry: 604801, - })).toThrow(); - }); - - it('should validate file field with attachment config', () => { - const field = { - name: 'resume', - label: 'Resume', - type: 'file' as const, - required: true, - multiple: false, - fileAttachmentConfig: { - maxSize: 5242880, // 5MB - allowedTypes: ['.pdf', '.docx'], - virusScan: true, - virusScanProvider: 'clamav' as const, - storageProvider: 'main_storage', - storageBucket: 'user_uploads', - storagePrefix: 'resumes/', - }, - }; - - const result = FieldSchema.safeParse(field); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.fileAttachmentConfig?.maxSize).toBe(5242880); - expect(result.data.fileAttachmentConfig?.allowedTypes).toHaveLength(2); - expect(result.data.fileAttachmentConfig?.virusScan).toBe(true); - } - }); - - it('should validate image field with attachment config', () => { - const field = { - name: 'profile_picture', - label: 'Profile Picture', - type: 'image' as const, - required: false, - fileAttachmentConfig: { - maxSize: 2097152, // 2MB - allowedTypes: ['.jpg', '.jpeg', '.png', '.webp'], - imageValidation: { - minWidth: 200, - maxWidth: 2048, - minHeight: 200, - maxHeight: 2048, - aspectRatio: '1:1', - generateThumbnails: true, - thumbnailSizes: [ - { name: 'thumb', width: 100, height: 100, crop: true }, - { name: 'small', width: 200, height: 200, crop: true }, - ], - autoRotate: true, - }, - storageProvider: 'main_storage', - storageBucket: 'user_uploads', - storagePrefix: 'avatars/', - }, - }; - - const result = FieldSchema.safeParse(field); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.fileAttachmentConfig?.imageValidation?.aspectRatio).toBe('1:1'); - expect(result.data.fileAttachmentConfig?.imageValidation?.thumbnailSizes).toHaveLength(2); - } - }); - - it('should work with comprehensive file upload configuration', () => { - const field = { - name: 'contract_document', - label: 'Contract Document', - type: 'file' as const, - description: 'Upload signed contract (PDF only)', - required: true, - multiple: false, - fileAttachmentConfig: { - minSize: 1024, // 1KB - maxSize: 52428800, // 50MB - allowedTypes: ['.pdf'], - allowedMimeTypes: ['application/pdf'], - virusScan: true, - virusScanProvider: 'virustotal' as const, - virusScanOnUpload: true, - quarantineOnThreat: true, - storageProvider: 'secure_storage', - storageBucket: 'legal_documents', - storagePrefix: 'contracts/', - allowMultiple: false, - allowReplace: true, - allowDelete: false, - requireUpload: true, - extractMetadata: true, - extractText: true, - versioningEnabled: true, - maxVersions: 5, - publicRead: false, - presignedUrlExpiry: 3600, - }, - }; - - const result = FieldSchema.safeParse(field); - expect(result.success).toBe(true); - if (result.success) { - expect(result.data.type).toBe('file'); - expect(result.data.fileAttachmentConfig?.virusScan).toBe(true); - expect(result.data.fileAttachmentConfig?.versioningEnabled).toBe(true); - expect(result.data.fileAttachmentConfig?.extractText).toBe(true); - expect(result.data.fileAttachmentConfig?.allowDelete).toBe(false); - } - }); - - it('should reject negative file sizes', () => { - expect(() => FileAttachmentConfigSchema.parse({ - minSize: -1, - })).toThrow(); - - expect(() => FileAttachmentConfigSchema.parse({ - maxSize: 0, - })).toThrow(); - }); - - it('should reject invalid image dimensions', () => { - expect(() => FileAttachmentConfigSchema.parse({ - imageValidation: { - minWidth: 0, - }, - })).toThrow(); - - expect(() => FileAttachmentConfigSchema.parse({ - imageValidation: { - maxHeight: -100, - }, - })).toThrow(); - }); - - it('should reject invalid versioning config', () => { - expect(() => FileAttachmentConfigSchema.parse({ - versioningEnabled: true, - maxVersions: 0, - })).toThrow(); - }); - - it('should reject minSize greater than maxSize', () => { - expect(() => FileAttachmentConfigSchema.parse({ - minSize: 1000, - maxSize: 500, - })).toThrow(); - - // Verify the specific error message - try { - FileAttachmentConfigSchema.parse({ - minSize: 1000, - maxSize: 500, - }); - } catch (error: any) { - expect(error.issues[0].message).toContain('minSize must be less than or equal to maxSize'); - } - }); - - it('should accept valid minSize and maxSize', () => { - const config = FileAttachmentConfigSchema.parse({ - minSize: 500, - maxSize: 1000, - }); - - expect(config.minSize).toBe(500); - expect(config.maxSize).toBe(1000); - }); - - it('should reject virusScanProvider without virusScan enabled', () => { - expect(() => FileAttachmentConfigSchema.parse({ - virusScan: false, - virusScanProvider: 'clamav', - })).toThrow(); - - // Verify the specific error message - try { - FileAttachmentConfigSchema.parse({ - virusScan: false, - virusScanProvider: 'clamav', - }); - } catch (error: any) { - expect(error.issues[0].message).toContain('virusScanProvider requires virusScan to be enabled'); - } - }); - - it('should accept virusScanProvider when virusScan is enabled', () => { - const config = FileAttachmentConfigSchema.parse({ - virusScan: true, - virusScanProvider: 'clamav', - }); - - expect(config.virusScan).toBe(true); - expect(config.virusScanProvider).toBe('clamav'); - }); }); }); diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index b2b379beab..b98df407cb 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -161,142 +161,6 @@ export const AddressSchema = lazySchema(() => z.object({ formatted: z.string().optional().describe('Formatted address string'), })); -/** - * Vector Configuration Schema - * Configuration for vector field type supporting AI/ML embeddings - * - * Vector fields store numerical embeddings for semantic search, similarity matching, - * and Retrieval-Augmented Generation (RAG) workflows. - * - * @example - * // Text embeddings for semantic search - * { - * dimensions: 1536, // OpenAI text-embedding-ada-002 - * distanceMetric: 'cosine', - * indexed: true - * } - * - * @example - * // Image embeddings with normalization - * { - * dimensions: 512, // ResNet-50 - * distanceMetric: 'euclidean', - * normalized: true, - * indexed: true - * } - */ -export const VectorConfigSchema = lazySchema(() => z.object({ - dimensions: z.number().int().min(1).max(10000).describe('Vector dimensionality (e.g., 1536 for OpenAI embeddings)'), - distanceMetric: z.enum(['cosine', 'euclidean', 'dotProduct', 'manhattan']).default('cosine').describe('Distance/similarity metric for vector search'), - normalized: z.boolean().default(false).describe('Whether vectors are normalized (unit length)'), - indexed: z.boolean().default(true).describe('Whether to create a vector index for fast similarity search'), - indexType: z.enum(['hnsw', 'ivfflat', 'flat']).optional().describe('Vector index algorithm (HNSW for high accuracy, IVFFlat for large datasets)'), -})); - -/** - * File Attachment Configuration Schema - * Configuration for file and attachment field types - * - * Provides comprehensive file upload capabilities with: - * - File type restrictions (allowed/blocked) - * - File size limits (min/max) - * - Virus scanning integration - * - Storage provider integration - * - Image-specific features (dimensions, thumbnails) - * - * @example Basic file upload with size limit - * { - * maxSize: 10485760, // 10MB - * allowedTypes: ['.pdf', '.docx', '.xlsx'], - * virusScan: true - * } - * - * @example Image upload with validation - * { - * maxSize: 5242880, // 5MB - * allowedTypes: ['.jpg', '.jpeg', '.png', '.webp'], - * imageValidation: { - * maxWidth: 4096, - * maxHeight: 4096, - * generateThumbnails: true - * } - * } - */ -export const FileAttachmentConfigSchema = lazySchema(() => z.object({ - /** File Size Limits */ - minSize: z.number().min(0).optional().describe('Minimum file size in bytes'), - maxSize: z.number().min(1).optional().describe('Maximum file size in bytes (e.g., 10485760 = 10MB)'), - - /** File Type Restrictions */ - allowedTypes: z.array(z.string()).optional().describe('Allowed file extensions (e.g., [".pdf", ".docx", ".jpg"])'), - blockedTypes: z.array(z.string()).optional().describe('Blocked file extensions (e.g., [".exe", ".bat", ".sh"])'), - allowedMimeTypes: z.array(z.string()).optional().describe('Allowed MIME types (e.g., ["image/jpeg", "application/pdf"])'), - blockedMimeTypes: z.array(z.string()).optional().describe('Blocked MIME types'), - - /** Virus Scanning */ - virusScan: z.boolean().default(false).describe('Enable virus scanning for uploaded files'), - virusScanProvider: z.enum(['clamav', 'virustotal', 'metadefender', 'custom']).optional().describe('Virus scanning service provider'), - virusScanOnUpload: z.boolean().default(true).describe('Scan files immediately on upload'), - quarantineOnThreat: z.boolean().default(true).describe('Quarantine files if threat detected'), - - /** Storage Configuration */ - storageProvider: z.string().optional().describe('Object storage provider name (references ObjectStorageConfig)'), - storageBucket: z.string().optional().describe('Target bucket name'), - storagePrefix: z.string().optional().describe('Storage path prefix (e.g., "uploads/documents/")'), - - /** Image-Specific Validation */ - imageValidation: z.object({ - minWidth: z.number().min(1).optional().describe('Minimum image width in pixels'), - maxWidth: z.number().min(1).optional().describe('Maximum image width in pixels'), - minHeight: z.number().min(1).optional().describe('Minimum image height in pixels'), - maxHeight: z.number().min(1).optional().describe('Maximum image height in pixels'), - aspectRatio: z.string().optional().describe('Required aspect ratio (e.g., "16:9", "1:1")'), - generateThumbnails: z.boolean().default(false).describe('Auto-generate thumbnails'), - thumbnailSizes: z.array(z.object({ - name: z.string().describe('Thumbnail variant name (e.g., "small", "medium", "large")'), - width: z.number().min(1).describe('Thumbnail width in pixels'), - height: z.number().min(1).describe('Thumbnail height in pixels'), - crop: z.boolean().default(false).describe('Crop to exact dimensions'), - })).optional().describe('Thumbnail size configurations'), - preserveMetadata: z.boolean().default(false).describe('Preserve EXIF metadata'), - autoRotate: z.boolean().default(true).describe('Auto-rotate based on EXIF orientation'), - }).optional().describe('Image-specific validation rules'), - - /** Upload Behavior */ - allowMultiple: z.boolean().default(false).describe('Allow multiple file uploads (overrides field.multiple)'), - allowReplace: z.boolean().default(true).describe('Allow replacing existing files'), - allowDelete: z.boolean().default(true).describe('Allow deleting uploaded files'), - requireUpload: z.boolean().default(false).describe('Require at least one file when field is required'), - - /** Metadata Extraction */ - extractMetadata: z.boolean().default(true).describe('Extract file metadata (name, size, type, etc.)'), - extractText: z.boolean().default(false).describe('Extract text content from documents (OCR/parsing)'), - - /** Versioning */ - versioningEnabled: z.boolean().default(false).describe('Keep previous versions of replaced files'), - maxVersions: z.number().min(1).optional().describe('Maximum number of versions to retain'), - - /** Access Control */ - publicRead: z.boolean().default(false).describe('Allow public read access to uploaded files'), - presignedUrlExpiry: z.number().min(60).max(604800).default(3600).describe('Presigned URL expiration in seconds (default: 1 hour)'), -}).refine((data) => { - // Validate minSize is less than or equal to maxSize - if (data.minSize !== undefined && data.maxSize !== undefined && data.minSize > data.maxSize) { - return false; - } - return true; -}, { - message: 'minSize must be less than or equal to maxSize', -}).refine((data) => { - // Validate virusScanProvider requires virusScan to be enabled - if (data.virusScanProvider !== undefined && data.virusScan !== true) { - return false; - } - return true; -}, { - message: 'virusScanProvider requires virusScan to be enabled', -})); - /** * Data Quality Rules Schema * Defines data quality validation and monitoring for fields @@ -545,17 +409,9 @@ export const FieldSchema = lazySchema(() => z.object({ currencyConfig: CurrencyConfigSchema.optional().describe('Configuration for currency field type'), // Vector field — flat dimensionality (the live authoring path). - // The renderer reads this flat sibling (objectui VectorField.tsx:11); the - // nested `vectorConfig` below is retained for back-compat but is a runtime - // no-op (no vector-index DDL consumes it). + // The renderer reads this flat sibling (objectui VectorField.tsx:11). dimensions: z.number().int().min(1).max(10000).optional().describe('Vector dimensionality (e.g., 1536 for OpenAI embeddings)'), - // Vector field config (DEAD — kept for back-compat; set the flat `dimensions` sibling instead) - vectorConfig: VectorConfigSchema.optional().describe('Configuration for vector field type (AI/ML embeddings)'), - - // File attachment field config - fileAttachmentConfig: FileAttachmentConfigSchema.optional().describe('Configuration for file and attachment field types'), - /** * Track this field's value changes on the record **activity timeline**. When * TRUE, the platform's activity writer renders each change as a human-readable @@ -576,9 +432,6 @@ export const FieldSchema = lazySchema(() => z.object({ // docs/audits/2026-06-dead-surface-disposition-plan.md (P0/P2 field prune): // encryptionConfig, maskingRule, auditTrail, cached, dataQuality. - /** Field Dependencies & Relationships */ - dependencies: z.array(z.string()).optional().describe('Array of field names that this field depends on (for formulas, visibility rules, etc.)'), - /** Layout & Grouping */ group: z.string().optional().describe('Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system")'), @@ -663,10 +516,6 @@ export type Address = z.infer; export type CurrencyConfig = z.infer; export type CurrencyConfigInput = z.input; export type CurrencyValue = z.infer; -export type VectorConfig = z.infer; -export type VectorConfigInput = z.input; -export type FileAttachmentConfig = z.infer; -export type FileAttachmentConfigInput = z.input; export type DataQualityRules = z.infer; export type DataQualityRulesInput = z.input; export type ComputedFieldCache = z.infer; diff --git a/packages/spec/src/data/object.test.ts b/packages/spec/src/data/object.test.ts index 6d1f39806e..290aaab1c8 100644 --- a/packages/spec/src/data/object.test.ts +++ b/packages/spec/src/data/object.test.ts @@ -712,7 +712,7 @@ describe('ObjectSchema', () => { }); // ============================================================================ -// Protocol Improvement Tests: displayNameField and recordName +// Protocol Improvement Tests: displayNameField / nameField // ============================================================================ describe('ObjectSchema - displayNameField', () => { @@ -778,49 +778,6 @@ describe('ObjectSchema - displayNameField', () => { }); }); -describe('ObjectSchema - recordName', () => { - it('should accept recordName with autonumber config', () => { - const result = ObjectSchema.parse({ - name: 'invoice', - fields: { - total: { type: 'number' }, - }, - recordName: { - type: 'autonumber', - displayFormat: 'INV-{YYYY}-{0000}', - startNumber: 1, - }, - }); - expect(result.recordName?.type).toBe('autonumber'); - expect(result.recordName?.displayFormat).toBe('INV-{YYYY}-{0000}'); - expect(result.recordName?.startNumber).toBe(1); - }); - - it('should accept recordName with text type', () => { - const result = ObjectSchema.parse({ - name: 'account', - fields: { - name: { type: 'text' }, - }, - recordName: { - type: 'text', - }, - }); - expect(result.recordName?.type).toBe('text'); - expect(result.recordName?.displayFormat).toBeUndefined(); - }); - - it('should accept object without recordName (optional)', () => { - const result = ObjectSchema.parse({ - name: 'task', - fields: { - title: { type: 'text' }, - }, - }); - expect(result.recordName).toBeUndefined(); - }); -}); - describe('ObjectSchema.create()', () => { it('should auto-generate label from snake_case name', () => { const result = ObjectSchema.create({ @@ -929,6 +886,31 @@ describe('ObjectSchema.create()', () => { expect(message).toContain('#2536'); }); + it('tombstone: dead metadata keys removed in 16.0 (#2377) carry upgrade guidance', () => { + const cases: Array<[string, unknown, string]> = [ + ['versioning', { enabled: true }, 'trackHistory'], + ['softDelete', { enabled: true }, 'hard deletes'], + ['search', { fields: ['name'] }, 'searchableFields'], + ['recordName', { type: 'autonumber' }, 'autonumber'], + ['keyPrefix', 'ACC', 'no effect'], + ]; + for (const [key, value, needle] of cases) { + let message = ''; + try { + ObjectSchema.create({ + name: 'demo', + fields: { status: { type: 'text' } }, + [key]: value, + } as Record as Parameters[0]); + } catch (e) { + message = (e as Error).message; + } + expect(message, `${key} should be rejected`).toContain(key); + expect(message, `${key} should cite #2377`).toContain('#2377'); + expect(message, `${key} should hint at the replacement`).toContain(needle); + } + }); + it('tombstone: removed detail block routes each job to its semantic role', () => { let message = ''; try { diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index e68320ea2e..265c572857 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -136,23 +136,6 @@ export const IndexSchema = lazySchema(() => z.object({ partial: z.string().optional().describe('Partial index condition (SQL WHERE clause for conditional indexes)'), })); -/** - * Search Configuration - * Defines how this object behaves in search results. - * - * @example - * { - * fields: ["name", "email", "phone"], - * displayFields: ["name", "title"], - * filters: ["status = 'active'"] - * } - */ -export const SearchConfigSchema = lazySchema(() => z.object({ - fields: z.array(z.string()).describe('Fields to index for full-text search weighting'), - displayFields: z.array(z.string()).optional().describe('Fields to display in search result cards'), - filters: z.array(z.string()).optional().describe('Default filters for search results'), -})); - /** * Tombstones for RETIRED tenancy keys — same doctrine as the top-level * `UNKNOWN_KEY_GUIDANCE` map below: a retired key's rejection must carry the @@ -274,42 +257,6 @@ export const ObjectRequiredPermissionsSchema = z.union([ export type PerOperationRequiredPermissions = z.infer; export type ObjectRequiredPermissions = z.infer; -/** - * Soft Delete Configuration Schema - * Implements recycle bin / trash functionality - * - * @example Standard soft delete with cascade - * { - * enabled: true, - * field: 'deleted_at', - * cascadeDelete: true - * } - */ -export const SoftDeleteConfigSchema = lazySchema(() => z.object({ - enabled: z.boolean().describe('Enable soft delete (trash/recycle bin)'), - field: z.string().default('deleted_at').describe('Field name for soft delete timestamp'), - cascadeDelete: z.boolean().default(false).describe('Cascade soft delete to related records'), -})); - -/** - * Versioning Configuration Schema - * Implements record versioning and history tracking - * - * @example Snapshot versioning with 90-day retention - * { - * enabled: true, - * strategy: 'snapshot', - * retentionDays: 90, - * versionField: 'version' - * } - */ -export const VersioningConfigSchema = lazySchema(() => z.object({ - enabled: z.boolean().describe('Enable record versioning'), - strategy: z.enum(['snapshot', 'delta', 'event-sourcing']).describe('Versioning strategy: snapshot (full copy), delta (changes only), event-sourcing (event log)'), - retentionDays: z.number().min(1).optional().describe('Number of days to retain old versions (undefined = infinite)'), - versionField: z.string().default('version').describe('Field name for version number/timestamp'), -})); - /** * Data Lifecycle (ADR-0057) * @@ -793,19 +740,11 @@ const ObjectSchemaBase = z.object({ * Absent/empty ⇒ no capability gate. */ requiredPermissions: ObjectRequiredPermissionsSchema.optional().describe('[ADR-0066 D3/⑤] Capabilities required to access this object (AND-gate) — `string[]` gates all CRUD, or a `{read,create,update,delete}` map gates per operation.'), - - // Soft delete configuration - softDelete: SoftDeleteConfigSchema.optional().describe('Soft delete (trash/recycle bin) configuration'), - - // Versioning configuration - versioning: VersioningConfigSchema.optional().describe('Record versioning and history tracking configuration'), // Data lifecycle (ADR-0057) — retention / rotation / archival contract, // enforced by the LifecycleService. Absent = `record` (today's behavior). lifecycle: LifecycleSchema.optional().describe('Data lifecycle contract (ADR-0057): class + retention/ttl/rotation/archive policies enforced by the platform LifecycleService.'), - // Partitioning strategy - /** * Logic & Validation (Co-located) * Best Practice: Define rules close to data. @@ -841,13 +780,14 @@ const ObjectSchemaBase = z.object({ /** * [ADR-0079] Canonical pointer to the object's PRIMARY title field — the one * real stored field (text / autonumber / formula→text) that is a record's - * human name. Pairs with `recordName` (the Salesforce Name / Record-Name - * model). Optional at the schema level for now (a hard required-refine is + * human name. Optional at the schema level for now (a hard required-refine is * staged so existing title-less metadata still parses). Resolve / derive via * `resolveDisplayField` from `@objectstack/spec/data` (display-name.ts), which * falls back to the deprecated `displayNameField` alias and then a derivation. + * Auto-naming (system-generated record names) is modelled as a `Field` of + * type 'autonumber' with `autonumberFormat`, designated as the `nameField`. */ - nameField: z.string().optional().describe('[ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). Pairs with recordName.'), + nameField: z.string().optional().describe('[ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title").'), /** * @deprecated [ADR-0079] Renamed to `nameField`. Still ACCEPTED as an alias: * the schema copies `displayNameField` onto `nameField` on parse when @@ -855,11 +795,6 @@ const ObjectSchemaBase = z.object({ * cross-repo back-compat). New metadata should set `nameField`. */ displayNameField: z.string().optional().describe('[DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField.'), - recordName: z.object({ - type: z.enum(['text', 'autonumber']).describe('Record name type: text (user-entered) or autonumber (system-generated)'), - displayFormat: z.string().optional().describe('Auto-number format pattern (e.g., "CASE-{0000}", "INV-{YYYY}-{0000}")'), - startNumber: z.number().int().min(0).optional().describe('Starting number for autonumber (default: 1)'), - }).optional().describe('Record name generation configuration (Salesforce pattern)'), titleFormat: TemplateExpressionInputSchema.optional().describe('[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField.'), /** * [ADR-0085] Semantic role: the object's most important fields, in priority @@ -919,10 +854,9 @@ const ObjectSchemaBase = z.object({ * Search Engine Config */ searchableFields: z.array(z.string()).optional().describe('Fields the `$search` query matches against (ADR-0061). Canonical default for the record picker, list quick-search and global search; views may narrow it. When unset, search auto-defaults to the name/title field plus short-text fields.'), - search: SearchConfigSchema.optional().describe('Search engine configuration'), - - /** - * System Capabilities + + /** + * System Capabilities */ enable: ObjectCapabilities.optional().describe('Enabled system features modules'), @@ -1001,9 +935,6 @@ const ObjectSchemaBase = z.object({ eligibility: z.string().optional().describe('CEL expression that must evaluate to true on the target record'), }).optional().describe('Public share-link policy (Notion/Figma-style link sharing)'), - /** Key Prefix */ - keyPrefix: z.string().max(5).optional().describe('Short prefix for record IDs (e.g., "001" for Account)'), - // [ADR-0085] The former `detail: { … }.passthrough()` UI-hints block is // REMOVED. Presentation intent lives in the cross-surface semantic roles // above (nameField / highlightFields / stageField / fieldGroups); per-page @@ -1109,6 +1040,29 @@ const UNKNOWN_KEY_GUIDANCE: Record = { '`defaultDetailForm` was never implemented and was removed from the spec ' + '(#2402). Curate the record page by assigning a custom Page schema; form ' + 'layout derives from `fieldGroups` + `Field.group`.', + softDelete: + '`softDelete` was removed from the spec in 16.0 (#2377, ADR-0049 ' + + 'enforce-or-remove) — there is no soft-delete/recycle-bin runtime, so it ' + + 'stored nothing and implied restore semantics that do not exist. Deletes ' + + 'are hard deletes; remove the key.', + versioning: + '`versioning` was removed from the spec in 16.0 (#2377, ADR-0049) — no ' + + 'record-versioning engine ever read it (it snapshotted no history). Use ' + + 'per-field `Field.trackHistory` for field-level history, or a data ' + + 'lifecycle policy (`lifecycle`) for retention.', + search: + '`search` (the SearchConfig block) was removed from the spec in 16.0 ' + + '(#2377, ADR-0049) — no search-engine config was consumed. Declare the ' + + 'indexed fields with `searchableFields` (ADR-0061); records stay queryable ' + + 'via the normal data API regardless.', + recordName: + '`recordName` was removed from the spec in 16.0 (#2377, ADR-0049) — it was ' + + 'never read. Auto-naming is modelled as a `Field` of type \'autonumber\' ' + + '(with `autonumberFormat`) designated as the object\'s `nameField`.', + keyPrefix: + '`keyPrefix` was removed from the spec in 16.0 (#2377, ADR-0049) — record ' + + 'ids are not prefixed from it (no Salesforce-style key-prefix runtime). ' + + 'Remove the key; it had no effect.', }; /** Levenshtein edit distance — backs the "did you mean" hint for typo'd keys. */ @@ -1308,8 +1262,6 @@ export type ObjectCapabilities = z.infer; export type ObjectIndex = z.infer; export type TenancyConfig = z.infer; export type ObjectAccessConfig = z.infer; -export type SoftDeleteConfig = z.infer; -export type VersioningConfig = z.infer; export type LifecycleClass = z.infer; export type Lifecycle = z.infer; diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index c98025e6ef..ea5a501683 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -527,9 +527,6 @@ export const ActionSchema = lazySchema(() => z.object({ */ newTabUrl: z.string().optional().describe('Direct new-tab URL template ({recordId} placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself.'), - /** Execution */ - timeout: z.number().optional().describe('Maximum execution time in milliseconds for the action'), - /** ARIA accessibility attributes */ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), }).transform((data) => { diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index f728aaccb0..6adceebbd9 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -148,7 +148,7 @@ export const DashboardWidgetSchema = lazySchema(() => z.object({ /** * ADR-0021 — the semantic-layer `dataset` this widget binds to. The widget * selects the dataset's dimensions/measures BY NAME; the dataset owns the base - * object, allowed joins, intrinsic filter, dimensions, and certified measures, + * object, allowed joins, intrinsic filter, dimensions, and measures, * so numbers stay consistent across every surface. This is the single * author-facing analytics shape (the legacy inline `object` + `categoryField` * + `valueField` + `aggregate` query was removed in the single-form cutover). diff --git a/packages/spec/src/ui/dataset.form.ts b/packages/spec/src/ui/dataset.form.ts index 63267aa22f..7ac53ab1aa 100644 --- a/packages/spec/src/ui/dataset.form.ts +++ b/packages/spec/src/ui/dataset.form.ts @@ -56,7 +56,7 @@ export const datasetForm = defineForm({ label: 'Measures', description: 'Aggregatable values defined once and referenced by name. A measure is sum/avg/count/… of a field; a derived measure combines other measures (ratio/sum/difference/product). Measure-scoped filters and derived ops are edited per-row in the dataset designer.', fields: [ - { field: 'measures', type: 'repeater', required: true, helpText: 'Each: name, aggregate, field (optional for count), display format/currency, and a “certified” governance flag' }, + { field: 'measures', type: 'repeater', required: true, helpText: 'Each: name, aggregate, field (optional for count), and display format/currency' }, ], }, ], diff --git a/packages/spec/src/ui/dataset.test.ts b/packages/spec/src/ui/dataset.test.ts index b8d3264612..15b71e139d 100644 --- a/packages/spec/src/ui/dataset.test.ts +++ b/packages/spec/src/ui/dataset.test.ts @@ -16,9 +16,9 @@ const base = { }; describe('DatasetSchema', () => { - it('accepts a well-formed dataset and applies defaults (certified=false)', () => { + it('accepts a well-formed dataset', () => { const ds = DatasetSchema.parse(base); - expect(ds.measures[0].certified).toBe(false); + expect(ds.measures[0].name).toBe('revenue'); expect(ds.object).toBe('opportunity'); }); diff --git a/packages/spec/src/ui/dataset.zod.ts b/packages/spec/src/ui/dataset.zod.ts index cac0155a6c..8ab6ca4016 100644 --- a/packages/spec/src/ui/dataset.zod.ts +++ b/packages/spec/src/ui/dataset.zod.ts @@ -80,8 +80,6 @@ export const DatasetMeasureSchema = lazySchema(() => z.object({ * layer) when the aggregated field is a fixed-currency amount. */ currency: z.string().length(3).optional().describe('Display currency code (ISO 4217)'), - /** Governance: a human-blessed metric — the review checkpoint. */ - certified: z.boolean().default(false).describe('Blessed metric (governance checkpoint)'), /** * Derived measure — computed from OTHER measures in this dataset by name * only. e.g. `{ op: 'ratio', of: ['won_amount', 'total_amount'] }`. @@ -208,7 +206,7 @@ export const DatasetSchema = lazySchema(() => z.object({ * object: 'opportunity', * include: ['account'], * dimensions: [{ name: 'region', field: 'account.region' }], - * measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount', certified: true }], + * measures: [{ name: 'revenue', aggregate: 'sum', field: 'amount' }], * }); * ``` */