Specification structure
한국어. This document defines field and list declarations. Form instances, repeated row keys and caching are defined in form runtime. Implementation and deployment status are recorded in features.
Package API
CRUDUI starts at package version 0.0.1. File names, public APIs and internal
identifiers describe their roles without implementation-generation markers.
Package roots expose the current validator and form renderers, including the
framework-independent @polyspec/crudui-generator-html renderer. Replaced versioned
paths have no compatibility aliases.
The machine-readable contract is maintained in
contracts/schema/crudui.schema.json. It validates
form, list and detail declaration shapes. make docs-schema checks this source against
shared fixtures without generating or replacing its rules.
Member order
A specification is JSON data. JavaScript receives it as plain objects, which list member names
that are array indexes (the decimal integers from 0 to 4294967294 written without leading zeros)
first in ascending numeric order and all other names in the order they were written. Every
runtime uses this order for every object in a specification: fields in properties, list columns,
detail fields, items value-to-label maps, the keys of every bucket, condition maps, composition files and
the objects composition produces, and compiled templates. "Declaration order" in these documents
means this order. For example, properties written as b, 10, a compile, render and validate
in the order 10, b, a in every runtime.
Arrays keep their written order; a choice list declares choices whose values are integer-like in their written order.
Record data keeps the order in which it arrives: form data, list rows, detail records and
options.data are never reordered, and ordered JSON preserves
numeric member names there.
Fields
A form root is a group with a properties field map. Property names define
data paths. A field separates structure, content and behavior as follows:
- CUSC-001 Structure — Keys:
type,name,default,properties,items,multiple,lang; Contract: Define values, nested groups, choices and repetition. - CUSC-002 Content — Keys:
label,description,placeholder,prepend,append,help,content; Contract: Accept text or a language map.contentis the control text of a button or action field. - CUSC-003 Validation — Keys:
validate,messages; Contract:validatedefines rules;messagesoverrides a rule's error message by rule name. - CUSC-004 Appearance — Keys:
design; Contract: Define visibility and styles for specific DOM nodes. - CUSC-005 Behavior — Keys:
behavior; Contract: Preserve event scripts without expression evaluation. - CUSC-006 Type options — Keys:
options; Contract: Store settings that apply to one widget type. - CUSC-007 Composition — Keys:
$ref,$patch; Contract: Load, merge and modify field definitions before binding data.
Content is resolved to text for the display language in the same way in every runtime, for form
fields and for list and detail display settings alike. A string is itself. A language map yields
the first non-empty string among its entry for the display language, en, ko and its first key;
entries that are not strings are skipped. Any other value (a number, a boolean, an array, or a map
without such an entry) is empty text.
Dependent settings remain under their subject: repeated row settings under
multiple, language input settings under lang, widget settings under options
and dynamic choice descriptors under items. Role and structural settings can
use false, true or an object where the type definition permits them. false
disables a setting; true selects defaults. The
TypeScript types define accepted
declaration shapes. Parsers preserve declared keys; validators reject forbidden
schema keys instead of silently discarding them.
type: group
properties:
email:
type: email
label: { en: Email, ko: 이메일 }
validate: { required: true, email: true }
design:
show: ".enabled"
class: { ".priority == 'high'": text-danger, true: "" }
Range fields
A range field holds one number chosen on a slider. Its bounds and its increment are its
validation rules: validate.range is [minimum, maximum] and validate.step is the increment.
Both are required and both are literal values, not expressions or condition maps. The slider
moves from the minimum in steps, so the minimum is a multiple of the step and every position of
the slider passes both rules. Every validator checks the submitted value with these rules
(numbers): a value passes when it is numeric, within the bounds and
a multiple of the step, and an empty value passes unless required is declared. append is the
unit written after the current value. The form markup defines the
control.
volume:
type: range
label: Volume
default: 50
append: "%"
validate: { range: [0, 100], step: 5 }
Compilation checks a range field after its behavior and fails with INVALID_FORM_INPUT and an
empty location. Multiples are decided exactly, as the step rule decides them.
- CUSC-008
validate.rangeabsent, or not two finite numbers with the minimum not above the maximum:Invalid validate.range at {path}: expected [minimum, maximum] finite numbers with minimum not above maximum - CUSC-009
validate.stepabsent, or not a finite number above 0:Invalid validate.step at {path}: expected a finite number above 0 - CUSC-010 A minimum that is not a multiple of the step:
Invalid validate.range at {path}: expected a minimum that is a multiple of validate.step
Condition fields
A condition field holds one condition tree as its data, and its control edits
the tree as JSON text. Its rule is validate.conditionTree, which is required and is the literal
true or judge, not an expression or a condition map: every value of the field is then checked as
a condition tree, with judged text only under judge. A required tree also declares required.
The form markup defines the control, and the
form runtime defines how a browser reads its text back into the
tree and how a server reads a submitted tree.
pass:
type: condition
label: Pass condition
validate: { required: true, conditionTree: judge }
Compilation checks a condition field after its behavior and fails with INVALID_FORM_INPUT and an
empty location:
- CUSC-070
validate.conditionTreeabsent, or neithertruenorjudge:Invalid validate.conditionTree at {path}: expected true or judge
Structured condition editor
A condition field may declare design.editor: text, the textarea of the
form markup, which is also the editor of a field without the
member, or structured, which edits the tree as nested controls
(structured editor). A structured field may declare
options.fields, the field names that its comparisons choose from; without it the field of a
comparison is typed text.
opens_when:
type: condition
label: Opens when
validate: { conditionTree: true }
design: { editor: structured }
options: { fields: [episodes.e1.status, clues.size] }
Compilation checks these members after validate.conditionTree and fails with
INVALID_FORM_INPUT and an empty location:
- CUSC-083
design.editorof a field that is not a condition field:Invalid design.editor at {path}: unknown key; and of a condition field, a value other thantextorstructured:Invalid design.editor at {path}: expected text or structured - CUSC-084
options.fieldsof a field that is not a structured condition field, or that is not a list of strings that are not empty after trimming and of which no two are equal:Invalid options.fields at {path}: expected distinct field names of a structured condition editor
The editor offers what the rule of the field accepts and shows the value without loss:
- CUSC-085 The editor offers the eight
opvalues of CUSC-073 in that order. It offers judged text, as a condition to add and as a condition that it shows, only underconditionTree: judge. - CUSC-086 The editor shows a value as controls when the value is an empty value, which it shows
as an empty
allgroup, or when the value is a condition tree thatconditionTreeof the field accepts after every emptyfieldand every emptyjudgetext is taken as filled, because a condition that the editor adds starts empty. It shows no other value as controls and does not replace it: the field writes its textarea, visible, with the condition text of the value, and a named error that names the rule and the data path of the field (CUMK-068), so the author corrects the text; the value stays the data of the field, andconditionTreereports it when the form is validated.
Conditions and appearance
Conditions are values in the relevant setting. design.show controls visibility;
design.class and design.style apply to the main node. design.label,
design.wrapper, design.group and design.prepend target those nodes.
Condition maps select the value of the first matching expression. The literal key
true is the optional default, applied only after every other condition fails,
whatever its position; with no match and no default the result is null
(condition maps). In every conditional
setting, including design.show, design.class and design.style, a string is
an expression only when it parses completely under the
expression grammar; any other string is a literal, so
class: "modal fade in show" is class text. Dedicated conditional
metadata such as show_if, display_switch
and display_target is rejected in CRUDUI schemas.
Declared attributes
A form field declares attributes of the elements CRUDUI renders for it: design.attributes for
its control and design.wrapper.attributes for its node root. The
form markup defines the elements and how renderers write
them. Each is an object of attribute name to string value; a value is literal text, not an
expression or a condition map. A name is data- or aria- followed by a lowercase letter or a
digit and then lowercase letters, digits, -, _ or .. A name that CRUDUI writes on a control
or a node root is refused: data-field-path, data-lang, every name that starts with
data-crudui- or data-source-, data-name, data-rule-name, data-default,
data-is-default, data-type, data-height, data-upload-server, data-fileserver,
data-server, data-max-tags, data-keyword-min-length, data-delay, data-api-server,
data-max-width, data-min-width, data-max-height, data-min-height,
data-preview-max-width, data-preview-max-height and data-unsupported-type.
Only form fields accept attributes. A form button, list or detail design and the label,
group and prepend nodes reject attributes as an unknown key. Compilation checks
design.attributes after design.class and design.style and before the design nodes, and
design.wrapper.attributes after the wrapper's class and style. It checks every name in
declaration order and then every value, and fails with INVALID_FORM_INPUT and an empty location:
- CUSC-011 Not an object:
Invalid design.attributes at {path}: expected an object - CUSC-012 A name outside the rule or a refused name:
Invalid design.attributes.{name} at {path}: expected a data-* or aria-* name that crudui does not write - CUSC-013 A value that is not a string:
Invalid design.attributes.{name} at {path}: expected a string
Under the wrapper the key is design.wrapper.attributes.
Layout
A group field declares the layout of the fields inside it with design.layout, a literal string,
not an expression or a condition map:
- CUSC-014
inline: every field node inside the group, at any depth, is one row. The label column, whose width is--crudui-label-width, holds the label; the control column holds the control, the description below it and the errors. A field without a label leaves the label column empty, so the controls of all rows align. Group, collection and language nodes keep the stacked layout and pass the inline layout to the field nodes inside them. The rows of a repeated scalar field and the items of a language field are not field nodes and stay stacked. - CUSC-015
line: the child nodes of the group sit side by side on one line, for example a select followed by small buttons. In an inline layout the group is one row: its label is in the label column and the line in the control column. The child nodes of a line take no inline layout. - CUSC-016
stacked: the label above the control, the layout of a form without a declaration. A group declares it to end an inline layout it would inherit.
A group without design.layout inherits the layout of the enclosing group. A repeated group accepts
stacked and inline, which apply to the fields of its rows. The form markup
defines the output.
appearance:
type: group
label: Appearance
design: { layout: inline }
properties:
theme: { type: select, label: Theme, items: { light: Light, dark: Dark }, description: Applies to every window. }
font:
type: group
label: Font
design: { layout: line }
properties:
family: { type: select, items: { mono: Mono, sans: Sans } }
size: { type: text }
Compilation checks design.layout after design.attributes and before the design nodes, and
fails with INVALID_FORM_INPUT and an empty location. The form root takes no layout: compilation
checks its design.layout after its buttons. The JSON Schema describes a field declaration, so it
checks the root as a group and leaves the root's layout to compilation.
- CUSC-017
design.layoutof a field that is not a group, of a form button or of the form root:Invalid design.layout at {path}: unknown key, where{path}isformfor the root - CUSC-018 A value other than
stacked,inlineorline:Invalid design.layout at {path}: expected stacked, inline or line - CUSC-019
lineon a repeated group:Invalid design.layout at {path}: expected stacked or inline
The expression grammar defines the tokenizer, parser
and evaluator. Supported expressions include relative paths, wildcards, lists,
comparison, logic, membership and conditional values. Arithmetic, function calls
and JavaScript evaluation are unsupported. Event scripts in behavior are opaque
strings; evaluating them is outside the expression engine.
Condition trees
A condition tree is a JSON value that a field holds as its data: an application writes a
condition in it, such as the condition under which a step of a story opens, and evaluates it
itself. It is data, not a declaration: CRUDUI does not evaluate it, and it is separate from the
expressions and condition maps of the settings above. The rule
conditionTree checks that a value has this form. A
condition is an object of one of these forms, whose members may be written in any order:
- CUSC-072 A group:
{ "all": [ … ] }or{ "any": [ … ] }, an object whose only member isalloranyand whose value is a list of conditions. The list may be empty. - CUSC-073 A field comparison:
{ "field": …, "op": …, "value": … }, an object with exactly these three members.fieldis a string that is not empty after trimming.opis one ofeq,ne,lt,le,gt,ge,inandhas.valueis, forin, a list of strings, finite numbers and booleans, which may be empty, and for every otheropa string, a finite number or a boolean. - CUSC-074 A judged text:
{ "judge": … }, an object whose only member isjudgeand whose value is a string that is not empty after trimming. The application decides whether the text holds. A judged text is a condition only where the rule permits it. - CUSC-075 Depth: a condition tree is at most 32 levels deep. The root condition is level 1, and each
condition in the list of a group is one level below the group. A deeper value is not a condition
tree. The limit bounds the work of a check, so no value can exhaust the stack of a runtime, and it
keeps a tree, which nests two levels of JSON (an object and its list) for each of its levels, with
the form data around it, within the 128 levels of nesting that a JSON reader of a runtime takes by
default (
serde_jsonof Rust).
What a condition means (which data field names, how op compares, when a group holds) belongs
to the application that evaluates the tree.
opens_when:
type: textarea
validate: { conditionTree: true }
pass:
type: textarea
validate: { required: true, conditionTree: judge }
The data { "opens_when": { "all": [{ "field": "episodes.e1.status", "op": "eq", "value": "passed" }] }, "pass": { "any": [{ "field": "clues.size", "op": "ge", "value": 2 }, { "judge": "Jian finds the second recording." }] } } passes both rules; a judge member in opens_when fails its rule.
Languages and choices
Content translation and language input values are separate. Content uses maps
such as { en: Name, ko: 이름 }. lang creates one value input per configured
language, with only, frame, title and group_class settings. The default
languages are ko, en, ja and zh.
items can be a static array whose indexes are the values, a value-to-label map, a
choice list, a group source with from or a
dynamic source descriptor with model. Static labels can use
language maps. Generators preserve dynamic source settings; fetching records and executing
external widgets happen outside the generators.
Choice lists
A choice list is an array of { "value": …, "label": … } objects. Each value is a string or a
finite number, and the option value is its canonical text: a string
itself and a number as Number.prototype.toString writes it. Each label is a label as in a
value-to-label map. The choices keep the order of the list for any values.
An items array is a choice list when one of its elements is an object that has a value member
or a choices member. Every element must then be an object whose only members are value and
label, besides the appearance members in a choice or multichoice field, or,
in a select field, a group; every value must be a string or a finite number,
and no two values in the list, inside groups included, may have the same canonical text. Binding a field
whose items is a choice list that breaks one of these rules fails with INVALID_FORM_INPUT, the
message Invalid items at {path}: expected value and label pairs with distinct string or number values
and an empty location, where {path} is the data path of the field. The check runs before the field
type is evaluated. Select, choice, multichoice and search fields list the pairs as options in list
order, and a dummy field displays the label of its value, as they do for a value-to-label map.
in and the choice-label format
accept the same choice list.
Choose the form by the required order:
- CUSC-020 A value-to-label map lists its values in member order, so integer-like values
come first in ascending numeric order:
{ "1": "Yes", "0": "No" }displays No before Yes. Use a map when that order is the required order, for example for values that are not integer-like. - CUSC-021 A choice list keeps the written order for any values:
[{ "value": 1, "label": "Yes" }, { "value": 0, "label": "No" }]displays Yes before No. Use it when the values are integer-like or mixed and the written order is required. - CUSC-022 An array of labels keeps the written order with the indexes
0,1, … as the values.
A select field (the types select, dropdown and selectbox) or a search field (the types
search and autocomplete) that has no option writes a placeholder option, so that its select
element is never empty:
- CUSC-080 A select or search field whose
itemsgive no option (absentitems, an empty array, an empty map, or a dynamic source descriptor withmodel, whose records the generators do not fetch) writes exactly oneoptionelement, whose value is the empty string and whose text isselect:<option value="">select</option>. The text is the same in every display language and is not translated, and the option is never selected, whatever the value of the field. A field with at least one option writes no placeholder option. The value of the placeholder is an empty value (CUVR-080).
Choice groups
The choice list of a select field (the types select, dropdown and selectbox) may contain group
entries. A group is an object whose only members are label, a label as in a value-to-label map,
and choices, a list of one or more { "value": …, "label": … } choices. Groups and plain choices
may be mixed, and the options keep the written order: a plain choice is an option of the select and
the choices of a group are the options of one optgroup element, which the
form markup defines. A group does not contain another group, and its
choices have no appearance members. The values of all choices, inside and outside groups, are the
choices of the field: they are distinct by canonical text across the whole list, the value of the
field selects the option with the same text in any group, and in
accepts the same list.
Binding a field whose choice list breaks one of these rules fails as described above: a group with
another member, without label or choices, with choices that is not a list or is an empty list,
or with a choice that breaks the choice rules, a value that repeats the canonical text of a value in
the same or another group or outside groups, and a group in a field that is not a select field. A
group in the choice list of the choice-label format fails as
another member of a choice does there.
region:
type: select
label: Region
items:
- { value: auto, label: Automatic }
- label: Europe
choices:
- { value: eu-west, label: West }
- { value: eu-north, label: North }
- label: Asia
choices:
- { value: ap-east, label: East }
validate:
in:
- { value: auto, label: Automatic }
- { label: Europe, choices: [{ value: eu-west, label: West }, { value: eu-north, label: North }] }
- { label: Asia, choices: [{ value: ap-east, label: East }] }
Choice appearance
A choice in the choice list of a choice or multichoice field (the types choice, radio,
multichoice, checkboxes and checkcontainer) may declare its appearance: class and style
are literal strings applied to the label of the choice, and attributes are
declared attributes written on the input of the choice. The field's
design.group (class and style) applies to the element that holds the choices. A page
can lay the choices out with these, for example as a grid of color swatches whose label reads a
custom property that each choice sets in style. The form markup
defines the output. The choice lists of other fields, of in and of the choice-label format have
only value and label, and another member fails as described above.
accent:
type: choice
label: Accent
items:
- { value: blue, label: Blue, class: swatch, style: "--swatch-bg: #1d4ed8", attributes: { aria-label: Blue accent } }
- { value: green, label: Green, class: swatch, style: "--swatch-bg: #15803d" }
design:
group: { class: swatches }
Binding checks the appearance after the choice list rules, choice by choice in list order and in
each choice class, style and then attributes, every name before every value. It fails with
INVALID_FORM_INPUT and an empty location; {index} is the zero-based position of the choice:
- CUSC-023 A
classorstylethat is not a string:Invalid items.{index}.{member} at {path}: expected a string - CUSC-024
attributesthat is not an object:Invalid items.{index}.attributes at {path}: expected an object - CUSC-025 A name outside the rule or a refused name:
Invalid items.{index}.attributes.{name} at {path}: expected a data-* or aria-* name that crudui does not write - CUSC-026 A value that is not a string:
Invalid items.{index}.attributes.{name} at {path}: expected a string
Choices from a repeated group
A choice field (the types select, dropdown, selectbox, choice, radio, multichoice,
checkboxes and checkcontainer) without lang may take its choices from the rows of a repeated
group of the same form, for example a role that names one of the characters the form declares.
Its items is then a group source: an object with the member from and the optional members
label and choices. An items object that has the member from is a group source and never a value-to-label
map; a map that needs the value from is written as a choice list.
characters:
type: group
multiple: true
properties:
id: { type: text, validate: { required: true, unique: true } }
name: { type: text }
roles:
type: group
multiple: true
properties:
character:
type: select
items: { from: characters.*.id, label: name }
-
CUSC-061
fromis a path{group}.*.{value}.{group}is the property names from the root to a repeated group, joined with., and no group on that path except the last is repeated, so the group has one set of rows for the whole form.*stands for the rows of the group, and{value}is the name of a direct child field of the group that is not repeated, not a group, has nolangand does not hold a boolean (CUSC-068). The field that declares the source may be inside that group or anywhere else in the form. -
CUSC-062
labelis the name of a direct child field of the same group that is not repeated, not a group, has nolangand does not hold a boolean (CUSC-068). Withoutlabelthe label of a choice is its value. -
CUSC-063 The choices are the rows of the group in the order of its data, read when the form is rendered or the data is validated. A row gives the choice whose value is the canonical text of its
{value}field when that value is a string or a finite number whose canonical text is not empty after trimming; any other row gives no choice. A row whose canonical text is the same as that of an earlier row repeats it (CUSC-090). The label of a choice is the canonical text of the row'slabelfield when that value is a string or a finite number whose canonical text is not empty after trimming, and otherwise the value of the choice. A group without data has no choices. The field lists these choices as it lists a choice list of the same pairs. -
CUSC-090 The values of the choices are distinct, as those of a choice list are. Rendering lists a repeated value once, as the choice of its first row, and validation refuses data in which the rows of the group repeat a value: an input error with
INVALID_FORM_INPUT, an empty location and the messageGroup source value repeats an earlier row: {value} at {path}(choices from a repeated group, CUVR-087). The rows of a group come from the data that a form submits, so a repeated value is data that the choices cannot express and validation reports it instead of choosing one of the rows. -
CUSC-089
choicesis a list of literal choices that join the choices of the rows, such as the audienceplayerbeside the ids of the characters:{ "value": …, "label": … }pairs as in a choice list, each with only these two members, avaluethat is a string or a finite number whose canonical text is not empty after trimming, as every choice of a row is, and alabelas in a value-to-label map, no two values with the same canonical text. The choices of the field are the literal choices in written order, followed by the choices of the rows (CUSC-063). A row whose value has the canonical text of a literal choice would give that value twice, so it is refused, not dropped: when the choices are read, for a binding or a validation of the data, it fails withINVALID_FORM_INPUT, an empty location and the messageGroup source value repeats a literal choice at {path}: {value}, where{path}is the data path of the row's{value}field, such ascharacters.c4.id, and{value}its canonical text. Compilation checkschoiceslast, after the type of the field thatlabelnames; achoicesthat breaks these rules fails withInvalid items.choices at {path}: expected value and label pairs with distinct string or number values.audience: type: select items: { from: characters.*.id, label: name, choices: [{ value: player, label: Player }] } -
CUSC-071 The choices follow the data. A form instance lists them from its current data after every change of that data (
setValue,setData,addRow,copyRow,removeRow,moveRow,rekeyRow,undoandredo), so a change of a row of the source group, of its{value}field or of itslabelfield changes the choices that the next view of the instance lists. Listing the choices changes no value: a value that no choice matches any more stays in the data, and validation reports it (choices from a repeated group).
A browser binding keeps the choices that the page shows equal to those of the data it holds. The
React, Vue and Svelte Form components and a page of the HTML renderer render every view of the
instance, so they show the choices of each change. The browser validation binding of a
server-rendered form (form binding)
renders the widget of each field with a group source again from the data of its controls when it
binds and after every input or change event, and keeps each selected value that is still a
choice selected; a value that is no longer a choice is held by no control, so its data lacks it as
a native submission does.
Every validator checks a value against these choices
(choices from a repeated group). Compilation checks a group source
after the other declarations of the field (its behavior and a range field's rules included), in
this order: the members of items in declaration order, the field, from, the type of the field
that from names, label, the type of the field that label names and then choices (CUSC-089). It fails with
INVALID_FORM_INPUT and an empty location:
- CUSC-064 A member other than
from,labelandchoices:Invalid items.{key} at {path}: unknown key - CUSC-065 A field that is not a choice field or that has
lang:Invalid items.from at {path}: expected a choice field without lang - CUSC-066 A
fromthat is not a string of that form, or whose group or field breaks these rules:Invalid items.from at {path}: expected {group}.*.{field} for a field of a repeated group outside repeated groups - CUSC-067 A
labelthat is not the name of such a field of the group:Invalid items.label at {path}: expected the name of a direct child field of the group without multiple, properties or lang - CUSC-068 A
fromorlabelthat names a field that holds a boolean:Invalid items.{member} at {path}: {field} holds a boolean, expected a field whose values are strings or numbers, where{member}isfromorlabeland{field}is the declaration path of the named field, such ascharacters.active. A field holds a boolean when its type stores a boolean as its value: the typescheckboxandswitcher, whose data istrueorfalse(checkbox fields, CUMK-027, and switches). The rows of such a field would give no choice or label (CUSC-063), so compilation refuses it rather than listing no choices. A type added later whose stored value is a boolean falls under this rule and is added to this sentence.
Composition and validation
Composition resolves $ref, then applies $patch, then processes the resulting
field definitions. Missing references are load errors. Composition does not
depend on record values. Form compilation resolves composition once per template.
A $ref value is one file path or a list of file paths. A list resolves each path
in declaration order and merges the results, so a later path overrides an earlier
one on a shared key; an entry that is not a string is a load error. A path may
select a fragment of a file as (file.yml).key. A reference resolves to the field
map of the layer it selects, which is why a declaration whose root is a bare $ref
is that field map and not yet a specification.
The CLI static check also rejects unresolved composition. It does not treat checking an uncomposed field as successful reference resolution.
Validators inspect validate rules and current data. The
validation rules describe rule semantics. Shared cases in
tests/fixtures/expr, tests/fixtures/compose and tests/fixtures/validate
compare the TypeScript, PHP, Go and Rust implementations. SSR comparisons and
mounted DOM tests verify different behavior and are recorded separately.
A field whose value names an entry of another field, such as the id of a row of another repeated
group, declares validate.reference with the declaration path of that field, for example
reference: facts.id. The references define the ids that the
data declares there and the check of the value.
Lists
A list declares columns; row records are a separate buildList(spec, rows, options) argument. Each column defines a field path, label, format,
design and optional sortable. Lists share composition, conditions, appearance
and content translation with forms. They use display cells instead of inputs.
A list without columns fails with List specification must declare columns, and the
display format declarations define the checks of every list and
detail declaration.
- CUSC-027
text:truncate - CUSC-028
date:pattern - CUSC-029
number:decimals,thousands,prefix,suffix - CUSC-030
badge: Value-to-variantmapand translated labels - CUSC-031
link:href,target,text - CUSC-032
choice-label:items - CUSC-033
bool:trueandfalselabels,as - CUSC-034
image:width,height,alt - CUSC-035
html: Unescaped display HTML
format accepts a type string or an object; absent, false and true select
text. Format-specific settings stay in that object. Display formats
defines each format, the accepted list and detail input and the markup. sort, pagination,
search and actions declare page behavior. empty defines translated
empty-state content and description translated text shown before the list. The list model
is { columns, rows, pagination, sort, actions, empty, description, design }: sort is absent
when no sort field is declared, and description is the translated description, empty text
when none is declared. The core does not query a database, filter records or apply
server pagination. The caller supplies the current page and the total record count. When
pagination is enabled, the resolved model defaults perPage to 20, mode to pages,
and page to 1 when omitted. It also supplies pageCount (0 without a total, otherwise at
least 1, also when the total is zero). per_page is an integer of at least 1, and the
display rules define the declaration checks and the page-number window.
Every renderer emits previous, numbered and next page buttons. The current page
has aria-current="page" and is disabled; boundary previous/next buttons are disabled.
The buttons carry data-page; navigation and data fetching remain the caller's responsibility.
Details
A detail declares fields; one record is supplied separately to
buildDetail(spec, record, options). Each field uses the same read-only display
contract as a list cell: field, label, format and design. Detail fields
share composition, conditions, appearance, content translation and cell formats
with lists, but do not declare sorting or pagination. A detail declares actions as a list
does. buildDetail returns ordered display fields, the resolved actions and the evaluated
detail design; renderers consume that model and query no data.
The model is { fields, actions, design }; actions has the list action models in member
order and is empty when the detail declares no actions. Each field has the members key, label, format,
value, display and design, in that order: the list cell of the one record, preceded
by its key and translated label. value is null when the record has no value at the
field path, as it is for a list cell. A declaration that is not an object fails with
Detail specification must be an object, a declaration without fields with Detail specification must declare fields, and a record that is not an object with Detail record must be an object. Every runtime returns the same model and the same errors, and every
string renderer writes the same detail HTML as React's server rendering, including image
preload links before the definition list.
Acceptance criteria
- CUSC-036 Resolve composition before evaluating fields and report missing references.
- CUSC-038 Evaluate conditions with the expression parser, without JavaScript evaluation.
- CUSC-039 Preserve language input values independently of the display language.
The schema keeps structure, content, validation, appearance and widget options separate. Shared validation cases are compared in four languages and SSR output in three frameworks, and editable form behavior is verified with mounted views.
Repeated fields accept multiple.min and multiple.max as numeric row-count
limits, multiple.copy and multiple.sortable for row controls, multiple.title
for the child field whose value titles each row, multiple.controls for the
control position (default header) and multiple.header for static (default) or
sticky row headers. The form markup defines their rendering.
multiple: only, the same as multiple.only: true, declares rows that exist only in the data:
the data's keys are the rows, missing data has no row, and the form offers no row controls or
row operations. multiple.only combines with title, header and keys and excludes min, max,
copy, sortable, controls and onclick. Compilation reports another string as
Invalid multiple at {path}: expected a boolean, only or an object, a non-boolean only as
Invalid multiple.only at {path}: expected a boolean, and an excluded key beside only: true as
Invalid multiple.{key} at {path}: unknown key.
Instance collection keys identify rows; the schema does not define hidden
identity fields. See form runtime for row operations.
Form compilation rejects a wrong value type in multiple, lang and design with
INVALID_FORM_INPUT and the message Invalid {key} at {path}: expected {expected}. {path} is the field's structural path, such as companies.name.
A condition map is a non-empty object. A button or action field takes its control
text only from content; text is not a field compatibility key. A form without buttons has one submit
button; a button or link needs text and a link needs href (see the
form markup).
- CUSC-041
multiple: Boolean,onlyor object - CUSC-081 Validation refuses at load a field whose
multipleis present and is not a boolean, the stringonlyor an object,nulland a list included, withINVALID_RULE_PARAMETER, the messageInvalid multiple at {path}: expected a boolean, only or an objectand the field's declaration path as its location and{path}. It checks fields in declaration order, each field before its children, and themultipleof a field before its group source and its rules (parameter errors). - CUSC-042
multiple.only: Boolean - CUSC-043
multiple.min,multiple.max: Number - CUSC-044
multiple.copy,multiple.sortable: Boolean - CUSC-045
multiple.title: Name of a direct child of a repeated group that is not repeated, not a group and has nolang - CUSC-046
multiple.controls:header,footeroroutline - CUSC-047
multiple.header:staticorsticky - CUSC-048
lang: Boolean or object - CUSC-049
lang.only: List of language-code strings or object - CUSC-050
design: Boolean or object - CUSC-051
content: Text or a language map; the control text of a button or action field - CUSC-052
messages: Object of registered rule name to message string - CUSC-053
buttons: List of buttons (type:submit,reset,buttonorlink); the form root only - CUSC-054
action: Object with stringmethod,urlandenctype; the form root only - CUSC-087
dataLimits: Object with integerdepth(1 to 512) andvalues(1 to 1000000); the form root only (data limits) - CUSC-055
design.show: Expression, boolean or condition map - CUSC-056
design.class,design.style: String or condition map - CUSC-057
design.label,design.wrapper,design.group,design.prepend: Object - CUSC-058
classandstyleof those nodes: String or condition map - CUSC-059
design.attributes,design.wrapper.attributes: Object of adata-*oraria-*name to a string; form fields only (declared attributes) - CUSC-060
design.layout:stacked,inlineorline; group fields only (layout) - CUSC-076
multiple.keys: Reference to a repeated group, besideonly: true(keyed maps)
Compilation checks fields in declaration order, each field before its children.
The schema closes multiple, lang, design, the design nodes and behavior: a key they do
not list fails with Invalid {bucket}.{key} at {path}: unknown key (for example
Invalid design.label.text at name: unknown key). Within a bucket, unknown keys are checked in
declaration order before the values. The order is buttons and action, multiple, lang,
design (then each node: label, wrapper, group, prepend) and behavior. options
and dynamic items sources stay open for type-specific settings; forbidden meta keys are
rejected everywhere. validate and messages keys are the registered rule names: the
meta-schema rejects any other name, and validation fails the load with UNKNOWN_RULE
(parameter errors). The meta-schema declares the parameter shapes of every registered
validation rule and rejects forbidden keys at every depth of every value it leaves open.
Keyed maps
A keyed map is a data-only collection whose keys are the ids of another repeated group of the form,
for example the stance of each character on a fact, or a note on each other character. The ids of a
repeated group are its row keys: rows are identified by their keys
(form runtime), not by a field named id. A field declares a keyed
map with multiple.keys beside only: true; the field may be a repeated scalar field or a repeated
group.
characters:
type: group
multiple: true
properties:
name: { type: text }
relation_notes:
type: text
multiple: { only: true, keys: ..characters }
facts:
type: group
multiple: true
properties:
truth: { type: text }
holders:
type: select
items: { knows: Knows, suspects: Suspects }
multiple: { only: true, keys: ..characters }
- CUSC-077 The value of
multiple.keysis a field reference to the repeated group, written as theenddateparameter is written (conditional parameters):.nameor a barenamenames a field beside the declaring field, each further leading dot moves one field level up, a row key leaving with its collection, and a reference with a dot inside and no leading dot names a path from the form root. In the declaration the reference therefore leaves as many fields of the declaring field's declaration path as it has leading dots, a bare name one and a path from the root all of them, and then follows its names. It names a repeated group when it does not leave the form root, has no empty name and no*, every name but the last names a field of the group reached so far that is a group and is not repeated, and the last names a repeated group. In the example both references leaverelation_notesorholdersand the repeated group around it and namecharacters. - CUSC-078 Compilation checks
multiple.keysaftermultiple.only:keyswithoutonly: truefails withInvalid multiple.keys at {path}: expected multiple.only: true, and a value that is not a string fails withInvalid multiple.keys at {path}: expected a reference to a repeated group, both withINVALID_FORM_INPUTand an empty location. Rendering does not readkeys: the rows of the field are its data's rows. Validation resolves the reference and checks the data's keys (keyed maps).
Widget and source settings
options preserves settings for the corresponding widget. Declaration acceptance
does not mean that the core executes an external widget. Search uses
keyword_min_length; map descriptors use marker_draggable, zoom and
geometry_type; tags use max_tags. Label settings include checkbox_label and
on_label. Container settings include collapse, expend, view_total,
stepper and blank_message. callback and event preserve widget scripts.
Dynamic choice descriptors can specify model, method, table, relations,
keys and api_server. A nested items collection supplies initial static
choices. Queries and endpoint calls happen outside the generators. A generator writes relations
into data-source-relations as JSON.stringify writes it, each number as ECMAScript
Number.prototype.toString writes it, so 1e21 is 1e+21, 100000000000000000000 keeps its
digits and -0 is 0.
- CUSC-088
relationsis written todata-source-relationsas the JSON text of its value; a missing or nullrelationsis written[].
Language configuration accepts a mode, an only language list or override map,
and group name, key, frame, title and group_class settings. Language
overrides can change validate, design, behavior and options.