Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/28005-thank-you-record-slot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@objectstack/spec': minor
'@objectstack/lint': minor
---

feat(spec): a form's thank-you panel can name the record's engine-generated fields, and the public submit answer has a declared shape

Clause-②: yes (widening)
Metadata-protocol: added

A public form's confirmation panel could not tell a web-to-case visitor their case number: `title` and `message` were plain text, and `POST /api/v1/forms/SLUG/submit` answers `{ id }` only.

- **The slot.** A form's `submitBehavior: { kind: 'thank-you', title, message }` may name a field of the record just submitted as `{{record.field_name}}`, the same token the `redirect` behavior's `url` takes. Any other brace is refused at parse, naming the offending text: `{record.x}`, `{{ record.x }}` and `{{case_number}}` are not the token.
- **Only engine-generated fields.** A named field must be one whose value the engine generates on insert: its type is in `RUNTIME_OWNED_FIELD_TYPES` (today `autonumber`). Any other stored value may have been set by a `beforeInsert` / `afterInsert` hook from records an anonymous visitor cannot read. `os validate`, `os build`, `os lint` and the publish gate on a `view` or `page` write refuse any other name (rule `thank-you-record-field`), naming the field and its type. This covers every form view shape and a page `object-form` component.
- **The answer shape.** `PublicFormSubmitResponseSchema` (`@objectstack/spec/api`) declares the submit answer as `id` plus an optional `fields` map holding exactly the named fields, and nothing else. `thankYouRecordFieldNames` (`@objectstack/spec/ui`) reads which fields a panel names.

The server fills `fields` and the console fills the tokens in follow-up releases. Until then, a panel that names a field shows the token text as written.
2 changes: 1 addition & 1 deletion content/docs/deployment/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ os compile --json # JSON output for CI pipelines
→ Normalizing stack definition...
→ Lowering inline handlers...
→ Validating protocol compliance...
→ Running author-time rules (56)...
→ Running author-time rules (57)...
→ Checking that every required capability has a provider installable in this edition...
→ Collecting package docs (ADR-0046)... 0 collected
→ Writing artifact...
Expand Down
2 changes: 1 addition & 1 deletion content/docs/deployment/validating-metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ A clean run walks the registry and reports timing:
Config: /path/to/support-desk/objectstack.config.ts
Load time: 21ms
→ Validating against ObjectStack Protocol...
→ Running author-time rules (56)...
→ Running author-time rules (57)...
→ Checking that every required capability has a provider installable in this edition...
→ Checking package docs (ADR-0046)...

Expand Down
2 changes: 1 addition & 1 deletion content/docs/getting-started/build-with-claude-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ visible: 'status != "resolved"'
◆ Validate
────────────────────────────────────────
→ Validating against ObjectStack Protocol...
→ Running author-time rules (56)...
→ Running author-time rules (57)...

✗ Author-time rules failed (1 issue)
• stack · action 'resolve_ticket' visible: bare reference `status` — a
Expand Down
18 changes: 15 additions & 3 deletions content/docs/references/api/protocol.mdx

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions content/docs/references/index.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Protocol reference — every schema by module
navTitle: Protocol Reference
description: Every schema published by @objectstack/spec — 1575 schemas across 14 protocol modules
description: Every schema published by @objectstack/spec — 1576 schemas across 14 protocol modules
---

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
Expand All @@ -21,7 +21,7 @@ counts are sums of the rows they head. Regenerate with
| Module | Pages | Schemas | Description |
| :--- | ---: | ---: | :--- |
| [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. |
| [API Protocol](/docs/references/api) | 34 | 453 | REST contracts, endpoints, routing, realtime, batch, discovery. |
| [API Protocol](/docs/references/api) | 34 | 454 | REST contracts, endpoints, routing, realtime, batch, discovery. |
| [Automation Protocol](/docs/references/automation) | 15 | 77 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
| [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
Expand All @@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
| [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
| [UI Protocol](/docs/references/ui) | 16 | 175 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
| **Total** | **204** | **1575** | 14 protocol modules |
| **Total** | **204** | **1576** | 14 protocol modules |

---

Expand Down Expand Up @@ -63,7 +63,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations.

## API Protocol

**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 453 schemas**
**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 454 schemas**

REST contracts, endpoints, routing, realtime, batch, discovery.

Expand Down Expand Up @@ -92,7 +92,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery.
| [`package-api-assembled.zod.ts`](/docs/references/api/package-api-assembled) | `AssembledInstalledPackage`, `GetInstalledPackageResponse`, `InstalledPackageAtEitherStage`, `ListInstalledPackagesResponse` |
| [`package-lifecycle.zod.ts`](/docs/references/api/package-lifecycle) | `DiscardPackageDraftsResponse`, `DuplicatePackageResponse`, `ImportUnmanagedPackageRequest`, `ImportUnmanagedPackageResponse`, `ListPackageCommitsResponse`, `PackageExportManifest`, `PackagePublishResult`, `ReassignOrphanedMetadataResponse`, `RevertPackageCommitResponse`, `RollbackToPackageCommitResponse` |
| [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `ValidationMode` |
| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `FlowRegistration`, `FlowRegistrationRefusal`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsRequest`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllPageHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` |
| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `FlowRegistration`, `FlowRegistrationRefusal`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublicFormSubmitResponse`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsRequest`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllPageHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` |
| [`query-adapter.zod.ts`](/docs/references/api/query-adapter) | `ODataQueryAdapter`, `OperatorMapping`, `QueryAdapterConfig`, `QueryAdapterTarget`, `RestQueryAdapter` |
| [`realtime.zod.ts`](/docs/references/api/realtime) | `RealtimeConfig`, `RealtimeEvent`, `RealtimeEventType`, `RealtimePresence`, `Subscription`, `SubscriptionEvent`, `TransportProtocol` |
| [`realtime-shared.zod.ts`](/docs/references/api/realtime-shared) | `BasePresence`, `PresenceStatus`, `RealtimeRecordAction` |
Expand Down
8 changes: 8 additions & 0 deletions content/docs/references/ui/component.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,14 @@ Sort field and direction pair
| **group** | `string` | optional | Field group key (snake_case) whose members and presentation this section inherits, from the object's `fieldGroups`. Mutually exclusive with `fields`, and with every key the group itself declares (`name`, `label`, `description`, `visibleWhen`, and a `true` `collapsible` / `collapsed`) |
| **fields** | `(string \| { field: string; type?: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; options?: object[]; reference?: string; … } \| { name: string; label?: string; description?: string; type?: string; … })[]` | optional | The section's fields, in order — each a field name, the form view's `{ field, … }` entry overriding that object field, or an inline form field `{ name, type, … }`. Omit only when `group` supplies the members |

### Nested Shape: `ObjectFormProps.submitBehavior[kind='thank-you']`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **kind** | `'thank-you'` | ✅ | |
| **title** | `string` | optional | Heading of the confirmation panel. May name a field of the submitted record as `{{record.field_name}}`, and only a field the engine generates on insert (an `autonumber` field, e.g. a case number): a hook-derived value never reaches an anonymous visitor. |
| **message** | `string` | optional | Body of the confirmation panel. May name a field of the submitted record as `{{record.field_name}}`, and only a field the engine generates on insert (an `autonumber` field, e.g. a case number): a hook-derived value never reaches an anonymous visitor. |

### Nested Shape: `ObjectFormProps.submitBehavior[kind='redirect']`

| Property | Type | Required | Description |
Expand Down
8 changes: 8 additions & 0 deletions content/docs/references/ui/view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,14 @@ Form-view select option — the object-field option shape minus the per-option `
| **expiresAt** | `string` | optional | Expiration date/time in ISO 8601 format |
| **allowAnonymous** | `boolean` | optional (default: `false`) | Allow access without authentication |

### Nested Shape: `FormView.submitBehavior[kind='thank-you']`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **kind** | `'thank-you'` | ✅ | |
| **title** | `string` | optional | Heading of the confirmation panel. May name a field of the submitted record as `{{record.field_name}}`, and only a field the engine generates on insert (an `autonumber` field, e.g. a case number): a hook-derived value never reaches an anonymous visitor. |
| **message** | `string` | optional | Body of the confirmation panel. May name a field of the submitted record as `{{record.field_name}}`, and only a field the engine generates on insert (an `autonumber` field, e.g. a case number): a hook-derived value never reaches an anonymous visitor. |

### Nested Shape: `FormView.submitBehavior[kind='redirect']`

| Property | Type | Required | Description |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/ui/react-pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ objectstack validate
────────────────────────────────────────
→ Loading configuration...
→ Validating against ObjectStack Protocol...
→ Running author-time rules (56)...
→ Running author-time rules (57)...
→ Checking that every required capability has a provider installable in this edition...
→ Checking package docs (ADR-0046)...

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate.

| Dir | Sites |
|---|---|
| `api/` | 463 |
| `api/` | 464 |
2 changes: 2 additions & 0 deletions packages/lint/src/authoring-rule-wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@ describe('authoring-rule registry wiring (#4409)', () => {
'validateReferenceIntegrity',
// #25650 — a form open to anonymous intake declaring a file field.
'validatePublicFormFileFields',
// #28005 — a thank-you panel naming a field the engine does not generate.
'validateThankYouRecordFields',
...FAMILY,
]);
});
Expand Down
20 changes: 20 additions & 0 deletions packages/lint/src/authoring-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ import { validateFieldConsumers } from './validate-field-consumers.js';
import { validateSemanticRoles } from './validate-semantic-roles.js';
import { validateFormLayout } from './validate-form-layout.js';
import { validatePublicFormFileFields } from './validate-public-form-file-fields.js';
import { validateThankYouRecordFields } from './validate-thank-you-record-fields.js';
import { validateSeedReplaySafety } from './validate-seed-replay-safety.js';
import { validateAssignmentRules } from './validate-assignment-rules.js';
import { validateSeedStateMachine } from './validate-seed-state-machine.js';
Expand Down Expand Up @@ -1612,6 +1613,25 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
runtimeTypes: ['view'],
run: (stack) => validatePublicFormFileFields(stack),
},
// Ruling B on #28005 — a form's `thank-you` panel may name, as
// `{{record.field_name}}`, only a field the engine generates on insert
// (`RUNTIME_OWNED_FIELD_TYPES`): any other stored value may come from a hook
// the anonymous form grant cannot read, and the public submit answer never
// carries it. Gating, so all three commands run it.
//
// Runtime publish gate: a `view` or `page` write's snapshot carries
// `objects` as context, which is every input this rule reads. `object` is
// not declared, for the reason the file-field rule above gives.
{
name: 'validateThankYouRecordFields',
tier: 'gating',
input: 'parsed',
commands: ALL,
source: 'packages/lint/src/validate-thank-you-record-fields.ts',
surfaces: CLI_AND_RUNTIME,
runtimeTypes: ['view', 'page'],
run: (stack) => validateThankYouRecordFields(stack),
},
// ADR-0078 Phase 3 (Tier-A `action-locations`) — an action that declares no
// `locations` and that no view places by name renders on no surface at all.
// objectui#3142 made that measurable: four renderers used to show an
Expand Down
Loading
Loading