Skip to content

Commit 9b1dc99

Browse files
feat(spec,lint): a form's thank-you panel names the record's engine-generated fields, and the public submit answer has a declared shape (#28440)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoEJN3dFK66nJxSuhYejKnJK)_ Part of #28005 Clause-②: yes (widening) Metadata-protocol: added Skills-docs: follow-up owed — content/docs/ui/public-data-collection.mdx and content/docs/ui/forms.mdx (the thank-you panel's `{{record.field_name}}` slot, taught once the server fills the answer and the Console fills the tokens; teaching it now would advertise a panel that still prints the token text); no `skills/**` page names the thank-you panel's text (`git grep -n "thank-you" -- skills/` is empty) domain:spec#1 · os-tesla · session_01L9qibwKTredm7FjB4Z6DeX This is the spec half of ruling B (ruling record 6545210989). The server half (`POST /forms/SLUG/submit` fills `fields`) and the ui half (`FormPage.tsx` fills the tokens) follow on their own cards, which the seat files once this PR is up. ## 维护者速读 - **改了什么**:公开表单的「提交成功」面板(`thank-you` 的 `title` / `message`)可以用 `{{record.字段名}}` 引用刚提交的那条记录的字段,但只允许引擎在插入时自动生成的字段(目前就是自动编号 `autonumber`,比如案件号)。同时在 spec 里声明了公开表单提交接口的返回形状:`id`,加上面板引用到的那几个字段,别的什么都不带。 - **为什么改**:web-to-case 的访客提交后看不到自己的案件号。按裁决 B,只放引擎生成的字段,因为其它字段的值可能是 hook 从匿名访客无权读取的记录里算出来的,不能回显给匿名页面。 - **风险与回滚**:低。引用写法和 `redirect` 的 `url` 是同一个记号;写错形状(`{record.x}`、`{{ record.x }}`、`{{case_number}}`)在解析时被拒并说明正确写法;引用了非自动编号字段,在 `os validate` / `os build` / `os lint` 和 view、page 的发布门被拒,并点名字段和类型。现有示例、hotcrm 里没有任何面板文字带花括号(实测为零)。回滚即 revert。 - **先看**:`packages/lint/src/validate-thank-you-record-fields.ts`(判定规则,只读 `RUNTIME_OWNED_FIELD_TYPES`);`packages/spec/src/ui/view.zod.ts`(`THANK_YOU_TEXT_RE`、`checkThankYouText`、`thankYouRecordFieldNames`);`packages/spec/src/api/protocol.zod.ts`(`PublicFormSubmitResponseSchema`)。 - **两分钟验证**:`pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-thank-you-record-fields.test.ts`,预期 16/16 通过:自动编号字段放行,文本字段、公式字段、未声明字段、系统列各被拒并点名类型;把类型集合在测试里放宽一种类型即放行,无需改规则。 - **需要你做的**:批准三个新名字(见下文 Names for approval),其余无,席位落地。 ## What changes ### 1. The slot (`@objectstack/spec`, `ui/view.zod.ts`) The `thank-you` member's `title` and `message` stay `z.string()`: the renderer receives the text the author wrote. Each now carries a `.regex(THANK_YOU_TEXT_RE)`: every brace in the text must belong to a well-formed `{{record.field_name}}` token. That is the same token the `redirect` member's `url` takes (`SUBMIT_REDIRECT_URL_TOKEN_RE`), so there is one grammar and no second dialect. - **A malformed token is refused on its own terms**, in the style of `checkSubmitRedirectUrl`. `checkThankYouText` words the refusal from the input: it names the member (`title` or `message`), quotes the offending text, and gives the one spelling (`{{record.case_number}}`). It also says braces belong to that token alone, and that only an engine-generated field may be named. - **A regex, not a refine.** A `.regex()` projects into the published JSON Schema as a `pattern`, so the file carries the shape too. That means no new `x-dropped-refinements` site and no `dropped-refinements.baseline.json` row. The two alternatives in the pattern start on disjoint characters, so matching is linear. - **`thankYouRecordFieldNames(submitBehavior)`** is the one reading of which fields a panel names: `title` then `message`, each name once, in order of first appearance, and `[]` for any other kind. It is a pure function of the block. The lint rule judges exactly these names, and the server half fills exactly these fields. - The two members gain a `.describe()` that states the slot and its limit. The union's own describe, which is projected into the governed `skills/objectstack-ui/references/react-blocks.md`, is unchanged, so this PR touches no governed surface. ### 2. The authoring check (`@objectstack/lint`, `validate-thank-you-record-fields.ts`) - **Where it lives.** I measured whether the redirect token's "declared record fields" rule is cross-checked anywhere. It is not: no lint rule reads `submitBehavior.url`, and the `view.zod.ts` docblock only names the reference-integrity family as the right layer. The nearest existing site that resolves a form's object fields is `validate-public-form-file-fields.ts`. The new rule sits beside it, with the same registration: gating, all three commands, the runtime publish gate. It resolves the form's object with the same `anonymousFormObjectName` the anonymous doors use. - **One source.** A name is admitted only when its field's type is in `RUNTIME_OWNED_FIELD_TYPES` (`data/field.zod.ts`, today `autonumber`). The rule spells no field type of its own. It takes the set as a defaulted parameter only so a test can widen it as a double: the spec dist is externalised in lint's tests, so it cannot be module-mocked. - **What is refused,** naming the field, its type and the reason (a value a hook may have set from records the anonymous grant cannot read): - a declared field of any other type (`text`, `formula`, …); - a registry-injected column (`created_at`, `owner_id`, `id`), which is injected, not engine-numbered; - a name the object does not declare (the panel would print nothing). - **Which forms.** Every form that carries a `thank-you` panel, public or not: the bare form-view shape, the nested `form`, every `formViews` entry, the flattened `config` of a `viewKind: 'form'` item, and a page `object-form` component (`properties.submitBehavior`, object from `properties.objectName`, else the page walk's binding). The slot is one slot with one admission. A form that is internal today becomes public by flipping its `sharing`, and that is a write this rule would otherwise never see again. - **Runtime publish gate:** `runtimeTypes: ['view', 'page']`. Both snapshots carry `objects` as context, which is every input the rule reads. `object` is not declared, for the reason the file-field rule gives: an object write's snapshot carries no views or pages. - A form whose object does not resolve in the stack is not judged, the same boundary the file-field rule draws. ### 3. The answer shape (`@objectstack/spec`, `api/protocol.zod.ts`) I measured whether any spec schema names the `POST /forms/:slug/submit` answer. None does: `rest-route-ledger.ts` lists the route with no `responseSchema`. `PublicFormSubmitResponseSchema` now sits next to `CreateDataResponseSchema`: - `id: string`; - `fields` (optional): a map from field name to a string or number value, holding exactly the named fields' stored values, present only when the panel names at least one; - **`.strict()`,** so a key beyond these two is a conformance failure rather than a silent widening of what an anonymous caller is shown. The server half fills it and names it as the route's `responseSchema`. ⛔ `packages/rest/src/form-routes.ts` is untouched here. ### Names for approval The order asks for Tesla's approval of every new name. These are proposals; renaming before merge is a mechanical edit. | Name | Kind | Where | |---|---|---| | `PublicFormSubmitResponseSchema` (type `PublicFormSubmitResponse`) | REST answer schema | `@objectstack/spec/api` | | `fields` | key on that answer | the map of named field values | | `thankYouRecordFieldNames` | pure helper | `@objectstack/spec/ui` | | `thank-you-record-field` / `validateThankYouRecordFields` | lint rule id / function | `@objectstack/lint` | `THANK_YOU_TEXT_RE` and `checkThankYouText` are module-private. ## Tests - **Parse** (`packages/spec/src/ui/view-submit-thank-you-record-slot.test.ts`, 15): - A `message` with `{{record.case_number}}` parses, and the text arrives verbatim. Title tokens, several tokens, token-free text and an empty panel all parse. - Refused, each naming the offender and the one spelling, at the member's path: `{record.x}`, `{{ record.x }}`, `{{case_number}}`, an uppercase segment, a dotted path, and a stray `}`. - The refusal carries no tracker id or date. The published JSON Schema carries the shape as a `pattern`. - `thankYouRecordFieldNames` reads title then message, each once, and reads only well-formed tokens. It answers `[]` for other kinds and for non-objects. - **Answer schema** (`packages/spec/src/api/public-form-submit-response.test.ts`, 4): it accepts `{ id }` and `{ id, fields: { case_number } }`. It refuses another top-level key (`unrecognized_keys`), a missing `id` and a non-scalar value. - **Authoring check** (`packages/lint/src/validate-thank-you-record-fields.test.ts`, 16): - On a fixture object, an `autonumber` token passes. `text` and `formula` tokens are refused, and the message names the field and its type. An undeclared name and the injected `created_at` / `id` are refused too. - Every form shape is judged: `form`, `formViews.*`, the `viewKind: 'form'` config, the bare form view, a public form, and a page `object-form`. A list-shaped `fields` is read, and the object resolves from the view. - **One source:** widening `RUNTIME_OWNED_FIELD_TYPES` in a test double (`+ 'formula'`) admits the formula token with no other edit. The rule source imports the set from `@objectstack/spec/data` and spells no `'autonumber'`. - Registration is pinned: gating, three commands, `['view', 'page']`. The runtime publish gate refuses a `view` write naming a text field and admits the same write naming the autonumber field. - **Existing pins updated:** - The publish-gate rule rosters name the new rule in `authoring-rule-wiring.test.ts`, `runtime-gate.test.ts` (3 sites) and `runtime-gate.page-visibility-predicate-writes.test.ts` (2 sites). - `type-alias-convention.pin.test.ts` pins `PublicFormSubmitResponseSchema` as isomorphic (805 → 806, with the reason in the count's ledger comment). <details><summary>Ablation (one-time, restored)</summary> Done through `node scripts/ablation-replace.mjs` on `packages/lint/src/validate-thank-you-record-fields.ts`. The suite imports the rule from `src/`, so no build sits between mutation and run. The type gate is the anchor `if (type !== undefined && admitted.has(type)) return undefined;`, which went x1 → x0, replaced by an unconditional `return undefined;`. - Blob `014ba032cba7` → `e040658efab9`. - `validate-thank-you-record-fields.test.ts`: 9 failed | 7 passed. The text and formula refusals, every form-shape case, the page case, the one-source widening case and the runtime gate's refusal all went red. - Restored: blob `014ba032cba7` = HEAD, `git diff HEAD` empty. </details> ## Acceptance notes - **A brace in panel text is now refused unless it is the token.** Before this PR, `title` / `message` accepted any text. Text with a brace that is not a well-formed token (a literal `{`, `{{ x }}`) now fails to parse. The order asked for this, mirroring the redirect refine. I measured the corpus: no authored `thank-you` title or message in `examples/`, the non-test `packages/` sources or `hotcrm` contains a brace. I copied the claim's declaration lines (`yes (widening)`, `added`) unchanged. The seat decides whether this face changes the Clause-② arm. - **`{{record.id}}` is refused.** `id` is not a `RUNTIME_OWNED_FIELD_TYPES` type. The answer already carries `id` at the top, so admitting it would be a separate decision. - **Translations are not judged.** The i18n checks (`validate-translation-references.ts`) read only the key address of `…submitBehavior.title/message`, never its text, so a token in the text breaks no translation check (measured). A translated panel could name a field the base text does not name. The server half fills only the base text's names, so such a token would print as written; it never leaks. Whether to judge translated text is the ui/server cards' question. - **An `object` write is not re-judged.** An object write's snapshot carries no views or pages, so retyping a named field under a published panel is caught by the CLI commands and by the form's next publish. This is the same boundary as the file-field rule. - Until the server and ui halves land, a panel that names a field shows the token text verbatim. The changeset says so. ## Verification The branch head is `eeb6adb2b`; each line names the commit it ran on where that differs. Heavy steps ran under `scripts/pm/os-verify-lock.sh`, and every exit code was captured before any pipe. - **Package suites:** - `pnpm --filter @objectstack/spec test` on `9d1b0b28`: 774 of 775 files passed (22847 tests, 1 todo). The one red was the isomorphic-pin count (805 → 806), fixed in this PR, and that file then passed 3/3. - `pnpm --filter @objectstack/lint test` on `9d1b0b28`: 183 files, 7698 passed, 5 skipped. - **Typecheck** on `47c5f03c4`: `pnpm --filter @objectstack/lint typecheck && pnpm --filter @objectstack/spec typecheck` exit 0. - **Generated artefacts** on `1bc2dd670`: `pnpm --filter @objectstack/spec check:generated --fix` rebuilt spec and regenerated api-surface, export-origins, declaration-map, the reference docs and the strictness ledger. It then re-checked all five green. A later `check:generated` exit 0 is in the rerun below. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 103 commands on `9d1b0b28`, and 104 on `eeb6adb2b` (+ `check:cli-examples-parity`, from the docs edit). - First sweep on `9d1b0b28`: 91 exit 0. - One real finding: `check-comment-mask-adoption` flagged a private comment stripper in my new lint test. I removed it. - Eleven reds read built output and had no build to read: ten exit 3 (prerequisite) and `check:dts-closure` / `check:generated` exit 1 (missing or stale dist). - After a full `turbo run build` of `./packages/**` and `./examples/**`, I re-ran all twelve on `b96858477`. One more real finding: `check:docs-transcript-drift` said four CLI transcripts print `author-time rules (56)`, and the registry now derives 57. I updated those four pages, and the gate exits 0 on `eeb6adb2b`, as does the new `check:cli-examples-parity`. - The final union of all 104 on `eeb6adb2b` is in the `os-dev-report` comment on #28005, reconciled with `dispatch-gates.mjs --ran`. <details><summary>First sweep, 9d1b0b2 (103)</summary> ``` node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-adr-links.mjs :: exit 0 node scripts/check-adr-links.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 1 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-dev-prereqs.mjs --self-test :: exit 0 node scripts/check-doc-route-spelling.mjs --advisory :: exit 0 node scripts/check-doc-route-spelling.mjs --self-test :: exit 0 node scripts/check-docs-section-name.mjs :: exit 0 node scripts/check-docs-section-name.mjs --self-test :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-issue-citations.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 node scripts/check-section-landing-index.mjs :: exit 0 node scripts/check-section-landing-index.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 3 pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 3 pnpm --filter @objectstack/spec run check:api-surface :: exit 0 pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0 pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0 pnpm --filter @objectstack/spec run check:docs :: exit 0 pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 3 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm --filter @objectstack/spec run check:empty-state :: exit 0 pnpm --filter @objectstack/spec run check:entry-nameability :: exit 3 pnpm --filter @objectstack/spec run check:export-origins :: exit 0 pnpm --filter @objectstack/spec run check:exported-any :: exit 3 pnpm --filter @objectstack/spec run check:generated :: exit 1 pnpm --filter @objectstack/spec run check:liveness :: exit 0 pnpm --filter @objectstack/spec run check:llms-txt :: exit 0 pnpm --filter @objectstack/spec run check:skill-examples :: exit 3 pnpm --filter @objectstack/spec run check:skill-refs :: exit 0 pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:corpus-claim-drift :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:doc-anchors :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:doc-frontmatter :: exit 0 pnpm check:docs-audit-scope :: exit 0 pnpm check:docs-redirects :: exit 0 pnpm check:docs-single-h1 :: exit 0 pnpm check:docs-spec-enumerations :: exit 0 pnpm check:docs-transcript-drift :: exit 3 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 1 pnpm check:dual-build-cjs-loads :: exit 3 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:lean-entry-closure :: exit 3 pnpm check:logger-receiver-detach :: exit 0 pnpm check:merge-driver :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:objectql-double-limit :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-object-tenancy-census :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:pm-prior-rulings :: exit 0 pnpm check:published-files :: exit 0 pnpm check:published-readme-links :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:react-page-adapter-contract :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:role-word :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:spec-docblock-symbol-anchors :: exit 0 pnpm check:spec-parsed-alias :: exit 0 pnpm check:system-context-census :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:vendor-version-stamps :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` </details> <details><summary>Rerun after the full build: twelve on b968584, the last two on eeb6adb</summary> ``` node scripts/check-comment-mask-adoption.mjs :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0 pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0 pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0 pnpm --filter @objectstack/spec run check:exported-any :: exit 0 pnpm --filter @objectstack/spec run check:generated :: exit 0 pnpm --filter @objectstack/spec run check:skill-examples :: exit 0 pnpm check:docs-transcript-drift :: exit 1 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:docs-transcript-drift :: exit 0 pnpm check:cli-examples-parity :: exit 0 ``` </details> - **CI:** the full farm, and `test:repo`, are CI's; `Test Core` runs the latter on the PR. - **Dogfood:** not run. The title carries no `!` and the Clause-② arm is not `(narrowing)`. --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8e6acf4 commit 9b1dc99

29 files changed

Lines changed: 797 additions & 18 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': minor
4+
---
5+
6+
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
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: added
10+
11+
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.
12+
13+
- **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.
14+
- **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.
15+
- **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.
16+
17+
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.

‎content/docs/deployment/cli.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -771,7 +771,7 @@ os compile --json # JSON output for CI pipelines
771771
→ Normalizing stack definition...
772772
→ Lowering inline handlers...
773773
→ Validating protocol compliance...
774-
→ Running author-time rules (56)...
774+
→ Running author-time rules (57)...
775775
→ Checking that every required capability has a provider installable in this edition...
776776
→ Collecting package docs (ADR-0046)... 0 collected
777777
→ Writing artifact...

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,7 @@ A clean run walks the registry and reports timing:
759759
Config: /path/to/support-desk/objectstack.config.ts
760760
Load time: 21ms
761761
→ Validating against ObjectStack Protocol...
762-
→ Running author-time rules (56)...
762+
→ Running author-time rules (57)...
763763
→ Checking that every required capability has a provider installable in this edition...
764764
→ Checking package docs (ADR-0046)...
765765

‎content/docs/getting-started/build-with-claude-code.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ visible: 'status != "resolved"'
305305
◆ Validate
306306
────────────────────────────────────────
307307
→ Validating against ObjectStack Protocol...
308-
→ Running author-time rules (56)...
308+
→ Running author-time rules (57)...
309309
310310
✗ Author-time rules failed (1 issue)
311311
• stack · action 'resolve_ticket' visible: bare reference `status` — a

‎content/docs/references/api/protocol.mdx‎

Lines changed: 15 additions & 3 deletions
Large diffs are not rendered by default.

‎content/docs/references/index.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Protocol reference — every schema by module
33
navTitle: Protocol Reference
4-
description: Every schema published by @objectstack/spec — 1575 schemas across 14 protocol modules
4+
description: Every schema published by @objectstack/spec — 1576 schemas across 14 protocol modules
55
---
66

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

3939
---
4040

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

6464
## API Protocol
6565

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

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

@@ -92,7 +92,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery.
9292
| [`package-api-assembled.zod.ts`](/docs/references/api/package-api-assembled) | `AssembledInstalledPackage`, `GetInstalledPackageResponse`, `InstalledPackageAtEitherStage`, `ListInstalledPackagesResponse` |
9393
| [`package-lifecycle.zod.ts`](/docs/references/api/package-lifecycle) | `DiscardPackageDraftsResponse`, `DuplicatePackageResponse`, `ImportUnmanagedPackageRequest`, `ImportUnmanagedPackageResponse`, `ListPackageCommitsResponse`, `PackageExportManifest`, `PackagePublishResult`, `ReassignOrphanedMetadataResponse`, `RevertPackageCommitResponse`, `RollbackToPackageCommitResponse` |
9494
| [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `ValidationMode` |
95-
| [`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` |
95+
| [`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` |
9696
| [`query-adapter.zod.ts`](/docs/references/api/query-adapter) | `ODataQueryAdapter`, `OperatorMapping`, `QueryAdapterConfig`, `QueryAdapterTarget`, `RestQueryAdapter` |
9797
| [`realtime.zod.ts`](/docs/references/api/realtime) | `RealtimeConfig`, `RealtimeEvent`, `RealtimeEventType`, `RealtimePresence`, `Subscription`, `SubscriptionEvent`, `TransportProtocol` |
9898
| [`realtime-shared.zod.ts`](/docs/references/api/realtime-shared) | `BasePresence`, `PresenceStatus`, `RealtimeRecordAction` |

‎content/docs/references/ui/component.mdx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,14 @@ Sort field and direction pair
653653
| **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`) |
654654
| **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 |
655655

656+
### Nested Shape: `ObjectFormProps.submitBehavior[kind='thank-you']`
657+
658+
| Property | Type | Required | Description |
659+
| :--- | :--- | :--- | :--- |
660+
| **kind** | `'thank-you'` | ✅ | |
661+
| **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. |
662+
| **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. |
663+
656664
### Nested Shape: `ObjectFormProps.submitBehavior[kind='redirect']`
657665

658666
| Property | Type | Required | Description |

‎content/docs/references/ui/view.mdx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,14 @@ Form-view select option — the object-field option shape minus the per-option `
491491
| **expiresAt** | `string` | optional | Expiration date/time in ISO 8601 format |
492492
| **allowAnonymous** | `boolean` | optional (default: `false`) | Allow access without authentication |
493493

494+
### Nested Shape: `FormView.submitBehavior[kind='thank-you']`
495+
496+
| Property | Type | Required | Description |
497+
| :--- | :--- | :--- | :--- |
498+
| **kind** | `'thank-you'` | ✅ | |
499+
| **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. |
500+
| **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. |
501+
494502
### Nested Shape: `FormView.submitBehavior[kind='redirect']`
495503

496504
| Property | Type | Required | Description |

‎content/docs/ui/react-pages.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ objectstack validate
404404
────────────────────────────────────────
405405
→ Loading configuration...
406406
→ Validating against ObjectStack Protocol...
407-
→ Running author-time rules (56)...
407+
→ Running author-time rules (57)...
408408
→ Checking that every required capability has a provider installable in this edition...
409409
→ Checking package docs (ADR-0046)...
410410

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate.
1919

2020
| Dir | Sites |
2121
|---|---|
22-
| `api/` | 463 |
22+
| `api/` | 464 |

0 commit comments

Comments
 (0)