Slot form: the form vocabulary toilville-design#178 requires, and form_schema is untyped #23

Open
opened 2026-10-21 22:49:33 +00:00 by peterswimm · 0 comments
Owner

toilville-design#178 (slot form) is decided and accepted. It is the most-used pattern and most of the others build on it. Its engineering follow-up, in ville's words:

schema fields for help text, placeholder, options, rules, visibility and field groups; renderers for radio, checkbox, secret, file and entity; evaluateGate in Flutter; the required check ignoring hidden fields

The renderer half belongs to the clients. The schema half has no contract, and the part that does exist is untyped. That is what this issue is for.

Companion to #22, which covers the command vocabulary. This one is the form vocabulary. They meet at item 4.

What the spec says about a slot today: nothing

PatternSchema.form_schema is the whole declaration:

form_schema:
  type: object
  additionalProperties: true
  nullable: true

There is no Slot schema, no field kinds, no attributes. And PatternSchema does not declare attributes at all, which GET /patterns/{name} does emit (ritual-server/src/main.rs:2991) — so the spec is behind the server on the one field the slot list actually travels in.

The live path is the one the client calls legacy

macOS resolves slots like this (apps/rituals-macos/Networking/ForgeAPIClient+Patterns.swift:130):

  1. prefer a typed slots array from the server;
  2. otherwise synthesise from attributes.input_schema, an array of {key, label, type, required}.

Step 1 never fires. There is no slots column on core.patterns (ritual-core/src/schema/core.rs:624), and neither GET /patterns (main.rs:2925) nor GET /patterns/{name} emits a slots key. The only "slots" in the engine is ritual-core/src/wish_slots.rs, which is model-extracted wish slots in a tool-call result — a different concept entirely.

So every slot in production comes from step 2, which hardcodes:

mutability: "fungible",
defaultValue: nil

Two of the six documented slot attributes are therefore unreachable, and one of them is a #178 deliverable. ville's decision requires:

sealed: the value is shown clearly and is read-only. It is labelled with the word "Sealed" … It does not look disabled.

A sealed slot cannot be expressed, so that state is undeliverable. The client's own test says so out loud — ForgeAPIClientPatternsTests.swift:131: "synthesized bridge slots default to fungible — sealed is unrecoverable from the legacy shape". That assertion has been passing as a description of the bug.

GET /patterns (the list) also omits form_schema and form entirely, while the spec documents both on the shared PatternSchema. Both are optional, so nothing is strictly invalid, but form's description ("null otherwise") reads as present-but-null when the list omits the key.

What to add

1. A declared Slot, replacing the untyped form_schema

Today's vocabulary, from SlotDTO (ForgeAPIClient+Patterns.swift:44) and #178: key/name, label, type, required, mutability (sealed | fungible), default_value. Declare it, and settle the key vs name split while doing so — SlotDTO decodes name, the attributes.input_schema fallback reads key, and #178's text says key.

Then decide whether a slot list stays inside attributes, moves to a declared slots member (which the client already expects and nothing serves), or becomes a JSON Schema in form_schema with the extras beside it. All three are live in the code right now.

2. The field kinds #178 adds

type is text | textarea | enum | int | bool today. #178 decides these as well:

kind note
single choice small set → radio, longer → select; the renderer sets the threshold
multiple choice small set → checkboxes, longer → multi-select picker
date, time separate kinds, so a ritual can ask for either or both; both accept typing
wish or entity a searchable picker; the selected entity shows as the field value
file the system file picker, with the selected filename
secret masked by default; never accepted through --field, and a stored secret is never shown in clear text

secret is the one with a security consequence, not just a presentation one: #178 forbids it on the CLI flag path, so the contract has to mark it before the CLI can refuse it.

schemas/forge_block.json's InputElement already covers text (with multiline), number, date, time, toggle and choice (with multi_select and style). It has no secret, no file and no entity picker.

3. The per-field attributes

Against the current block vocabulary:

#178 needs state
help text InputBlock.hint ✔
placeholder TextInputElement.placeholder, NumberInputElement.placeholder ✔
required marker absent. AvailableCommand.required_inputs covers a command's inputs; a standalone slot form has nothing. #178 wants * with the accessible name "required"
a description per option absent. ChoiceOption is {label, value}. #181 requires "a short description for each", read with its option
a default per field absent on ChoiceInputElement. #181: "the schema supplies … the default (Done). The renderer does not hard-code the default"
sealed absent (see above)
field groups absent. "The ritual definition says which fields belong to each group. Groups are open by default." Explicitly not the section block
length_at_least absent. TextInputElement has max_length and no minimum
matches_pattern absent
equals (conditional visibility) absent

4. Where the rules live — settle this first

#178 asks for three field-level rules — equals for conditional visibility, length_at_least and matches_pattern for validation — and adds:

They may share one rule vocabulary, so authors do not write the same check twice.

There are two candidate homes and the code currently implies both:

  • the JSON Schema. AvailableCommand.input_schema is "JSON Schema of the command's input form", and JSON Schema already spells all three: minLength, pattern, and if/then for visibility. POST /patterns/{name}/validate validates against form_schema this way.
  • the block vocabulary. The rendered form is a command_form view made of ForgeBlocks, and a renderer renders blocks. A rule a renderer never reads cannot hide a field or show an error under it.

Deciding this is engineering, not design — #178 says so — but it has to be decided before any of items 2 and 3 can be written down, because it determines which document they go in. Note two constraints that follow from #178 regardless of the answer:

  • Hidden fields are ignored by required checks, validation and step gates, and are not submitted. So "required" and "visible" cannot be independent flags a renderer evaluates in any order.
  • GateExpression is unaffected. It stays a step-level mechanism and needs no new types. Validation, conditional visibility and step gating are three separate behaviours.

5. Not in scope

  • CLI prompt behaviour (hidden secret input, numbered choice lists, re-asking an invalid field) is decided in #178 and is client work, not contract work. The one contract-visible part is secret, item 2.
  • Which CLI. #178 distinguishes the Dart ritual_cli from the ritual binary; that split is a client question.

Acceptance

  • A Slot schema exists, form_schema is no longer the only declaration, and attributes is declared or the slot list has moved out of it.
  • Item 4 is decided and written down, before items 2 and 3 are specified.
  • A sealed slot is expressible end to end, and ForgeAPIClientPatternsTests.swift:131's assertion is replaced by one that proves it round-trips rather than one that records that it cannot.
  • secret is markable, so the CLI can refuse it through --field.
  • Each lands only after the server serves it, per CLAUDE.md. Sequence: spel → ritual-engine → here.

Blocks

  • toilville_rituals#583 — _ParametersPanel in ritual_execution_screen.dart cannot migrate to a served slot form until the form is expressible
  • #22 — meets it at item 4; a command's inputs render through the same vocabulary

Design side is closed: toilville-design#178 is decided and accepted. Nothing here needs a design answer.

[toilville-design#178](https://git.toilville.dev/Toilville/toilville-design/issues/178) (slot form) is decided and accepted. It is the most-used pattern and most of the others build on it. Its engineering follow-up, in ville's words: > schema fields for help text, placeholder, options, rules, visibility and field groups; renderers for radio, checkbox, secret, file and entity; `evaluateGate` in Flutter; the required check ignoring hidden fields The renderer half belongs to the clients. **The schema half has no contract, and the part that does exist is untyped.** That is what this issue is for. Companion to #22, which covers the *command* vocabulary. This one is the *form* vocabulary. They meet at item 4. ## What the spec says about a slot today: nothing `PatternSchema.form_schema` is the whole declaration: ```yaml form_schema: type: object additionalProperties: true nullable: true ``` There is no `Slot` schema, no field kinds, no attributes. And `PatternSchema` does not declare **`attributes`** at all, which `GET /patterns/{name}` does emit (`ritual-server/src/main.rs:2991`) — so the spec is behind the server on the one field the slot list actually travels in. ### The live path is the one the client calls legacy macOS resolves slots like this (`apps/rituals-macos/Networking/ForgeAPIClient+Patterns.swift:130`): 1. prefer a typed `slots` array from the server; 2. otherwise synthesise from `attributes.input_schema`, an array of `{key, label, type, required}`. Step 1 never fires. There is no `slots` column on `core.patterns` (`ritual-core/src/schema/core.rs:624`), and neither `GET /patterns` (`main.rs:2925`) nor `GET /patterns/{name}` emits a `slots` key. The only `"slots"` in the engine is `ritual-core/src/wish_slots.rs`, which is model-extracted wish slots in a tool-call result — a different concept entirely. So every slot in production comes from step 2, which hardcodes: ```swift mutability: "fungible", defaultValue: nil ``` **Two of the six documented slot attributes are therefore unreachable, and one of them is a #178 deliverable.** ville's decision requires: > sealed: the value is shown clearly and is read-only. It is labelled with the word "Sealed" … It does not look disabled. A sealed slot cannot be expressed, so that state is undeliverable. The client's own test says so out loud — `ForgeAPIClientPatternsTests.swift:131`: *"synthesized bridge slots default to fungible — sealed is unrecoverable from the legacy shape"*. That assertion has been passing as a description of the bug. `GET /patterns` (the list) also omits `form_schema` and `form` entirely, while the spec documents both on the shared `PatternSchema`. Both are optional, so nothing is strictly invalid, but `form`'s description ("null otherwise") reads as present-but-null when the list omits the key. ## What to add ### 1. A declared `Slot`, replacing the untyped `form_schema` Today's vocabulary, from `SlotDTO` (`ForgeAPIClient+Patterns.swift:44`) and #178: `key`/`name`, `label`, `type`, `required`, `mutability` (`sealed` | `fungible`), `default_value`. Declare it, and settle the `key` vs `name` split while doing so — `SlotDTO` decodes `name`, the `attributes.input_schema` fallback reads `key`, and #178's text says `key`. Then decide whether a slot list stays inside `attributes`, moves to a declared `slots` member (which the client already expects and nothing serves), or becomes a JSON Schema in `form_schema` with the extras beside it. All three are live in the code right now. ### 2. The field kinds #178 adds `type` is `text | textarea | enum | int | bool` today. #178 decides these as well: | kind | note | |---|---| | single choice | small set → radio, longer → select; the renderer sets the threshold | | multiple choice | small set → checkboxes, longer → multi-select picker | | `date`, `time` | **separate kinds**, so a ritual can ask for either or both; both accept typing | | wish or entity | a searchable picker; the selected entity shows as the field value | | file | the system file picker, with the selected filename | | secret | masked by default; **never accepted through `--field`**, and a stored secret is never shown in clear text | `secret` is the one with a security consequence, not just a presentation one: #178 forbids it on the CLI flag path, so the contract has to mark it before the CLI can refuse it. `schemas/forge_block.json`'s `InputElement` already covers text (with `multiline`), number, date, time, toggle and choice (with `multi_select` and `style`). It has no `secret`, no `file` and no entity picker. ### 3. The per-field attributes Against the current block vocabulary: | #178 needs | state | |---|---| | help text | `InputBlock.hint` ✔ | | placeholder | `TextInputElement.placeholder`, `NumberInputElement.placeholder` ✔ | | required marker | **absent.** `AvailableCommand.required_inputs` covers a command's inputs; a standalone slot form has nothing. #178 wants `*` with the accessible name "required" | | a description per option | **absent.** `ChoiceOption` is `{label, value}`. #181 requires "a short description for each", read with its option | | a default per field | **absent** on `ChoiceInputElement`. #181: "the schema supplies … the default (Done). The renderer does not hard-code the default" | | sealed | **absent** (see above) | | field groups | **absent.** "The ritual definition says which fields belong to each group. Groups are open by default." Explicitly *not* the `section` block | | `length_at_least` | **absent.** `TextInputElement` has `max_length` and no minimum | | `matches_pattern` | **absent** | | `equals` (conditional visibility) | **absent** | ### 4. Where the rules live — settle this first #178 asks for three field-level rules — `equals` for conditional visibility, `length_at_least` and `matches_pattern` for validation — and adds: > They may share one rule vocabulary, so authors do not write the same check twice. There are two candidate homes and the code currently implies both: - **the JSON Schema.** `AvailableCommand.input_schema` is "JSON Schema of the command's input form", and JSON Schema already spells all three: `minLength`, `pattern`, and `if`/`then` for visibility. `POST /patterns/{name}/validate` validates against `form_schema` this way. - **the block vocabulary.** The rendered form is a `command_form` view made of ForgeBlocks, and a renderer renders blocks. A rule a renderer never reads cannot hide a field or show an error under it. Deciding this is engineering, not design — #178 says so — but it has to be decided before any of items 2 and 3 can be written down, because it determines which document they go in. Note two constraints that follow from #178 regardless of the answer: - **Hidden fields are ignored by required checks, validation and step gates, and are not submitted.** So "required" and "visible" cannot be independent flags a renderer evaluates in any order. - **`GateExpression` is unaffected.** It stays a step-level mechanism and needs no new types. Validation, conditional visibility and step gating are three separate behaviours. ### 5. Not in scope - **CLI prompt behaviour** (hidden secret input, numbered choice lists, re-asking an invalid field) is decided in #178 and is client work, not contract work. The one contract-visible part is `secret`, item 2. - **Which CLI.** #178 distinguishes the Dart `ritual_cli` from the `ritual` binary; that split is a client question. ## Acceptance - [ ] A `Slot` schema exists, `form_schema` is no longer the only declaration, and `attributes` is declared or the slot list has moved out of it. - [ ] Item 4 is decided and written down, before items 2 and 3 are specified. - [ ] A sealed slot is expressible end to end, and `ForgeAPIClientPatternsTests.swift:131`'s assertion is replaced by one that proves it round-trips rather than one that records that it cannot. - [ ] `secret` is markable, so the CLI can refuse it through `--field`. - [ ] Each lands only after the server serves it, per `CLAUDE.md`. Sequence: spel → ritual-engine → here. ## Blocks - [toilville_rituals#583](https://git.toilville.dev/Toilville/toilville_rituals/issues/583) — `_ParametersPanel` in `ritual_execution_screen.dart` cannot migrate to a served slot form until the form is expressible - #22 — meets it at item 4; a command's inputs render through the same vocabulary Design side is closed: toilville-design#178 is decided and accepted. Nothing here needs a design answer.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Toilville/rituals-api-spec#23
No description provided.