This page is the record of a design review, not a description of shipped behaviour: GEP-0008 is a draft, and none of the form-* types is registered yet. Fifteen decisions were taken over five revisions, each recorded on the board with who took it; below, every decision is drawn as it will look — GEML on the left, the control a host would render on the right. By the GEP's own conformance clause a renderer may only show a disabled preview, so every control here is greyed out and inert.
Settled decided by the maintainer, or fixed in the proposal's text. Nothing remains open.
| Item | Decision | Status | By | |
|---|---|---|---|---|
| 1 | the form-* family | Child types carry the form- prefix: form-field form-group form-options form-note, all meaningful only inside a form, all ids in the form's scope — one rule. A form is therefore self-contained: geml get '#vendor' returns the whole form. §8.5 gains one sentence: an extension type name may not begin with a registered type name plus a hyphen. | Settled | maintainer |
| 2 | repeating groups | form-group repeats by nature — Ant Design's Form.List — with no multiple; required means at least one entry. It opens a scope; the address is #vendor#contacts#email. A non-repeating composite is not defined: visual grouping uses headings. Groups do not nest: a form-group sits directly in the form and holds only form-fields. Zero nested groups in the three trial forms; not nesting fixes the scope at two levels, the address at three segments, the fence at five =, and no tool walks a recursive scope. | Settled | maintainer |
| 3 | options | Carried by form-options: a table body, format / delim / src as on table, placed directly in the form. Contract unchanged: first column is the value, a label column is the text, further columns go to the handler. options= may name only a form-options in the same form; naming a table is an error. Reuse across forms is one src= file. | Settled | maintainer |
| 4 | the type= set | text · textarea · number · date · boolean · select · file — seven pure value shapes. group left the list and became form-group. | Settled | maintainer |
| 5 | unknown type value | Degrades to text with a warning, unknown-field-type. | Settled | maintainer |
| 6 | constraint attributes | pattern min max step maxlength accept go into the geml-form/v1 profile; declared, never evaluated; the handler enforces them. | Settled | maintainer |
| 7 | rich text | label= description= placeholder= share one rule: plain text, or, beginning with #, a reference to a form-note in the same form. form-note carries no for=; like form-options it is a passive resource several fields may share. The test has precedent: a src= value beginning with # is a reference. A placeholder pointing at a form-note is flattened to text by the renderer. Named form-note. | Settled | maintainer |
| 8 | description= | A one-line plain attribute, the grey line under the control. Longer guidance goes in a form-note, not in unbound prose. | Settled | maintainer |
| 9 | headings inside a form | Allowed. Ordinary document headings, document-level ids, not fields. | Settled | maintainer |
| 10 | placeholder= | A form-field attribute. | Settled | maintainer |
| 11 | textarea format= | Not wanted. | Settled | maintainer |
| 12 | value= on multiple | Names the single preselected option. | Settled | maintainer |
| 13 | form-* outside a form | error form-child-outside-form, for all four child types; a non-empty form-field body is a warning form-field-has-body. | Settled | maintainer |
| 14 | conditional logic / cross-field checks | Not carried by the document; handler only. | Settled | proposal text |
| 15 | Steps / tabs / buttons / feedback | Application layer: GEP-0009 profiles and geml-style. | Settled | proposal text |
The answer to "where does validation live". It did not change through the revisions.
.gemlThe form and its form-* children. What it says: which fields, of what type, required or not, which options, and declarations of format and range — keys admitted by the geml-form/v1 profile.
Does not: evaluate, validate or submit. §9.1, §8.3(5).
Decides the look: one page or Steps, whether groups collapse, dropdown or radio group, whether min/max show as hints.
Does not: judge right or wrong. It renders a disabled preview and never sees input.
Registered by the host under a name (handler=onboarding). The only party that ever holds a user's input: enforces the declared constraints, plus every business rule the document cannot carry.
Does not: decide how a field looks, or change the document.
A "constraint" is something declarable — a format or a range — written in the document. "Validation" is an act, and it happens only in the handler. A stylesheet holds no validation, only appearance.
form-* family: one form, four kinds of childThe maintainer's design. A complete form using all four child types and a heading inside the body; on the right, how it renders.
=, form-group at four, everything else at three. A form-field body is always empty. Every form-* id lives in the form's scope.=== meta profile = "geml-form/v1" === ===== form {#vendor handler=onboarding} ## Company {#company} === form-field {#legal-name label="Legal name" type=text required description="Exactly as on the business licence."} === === form-options {#entity-types format=csv delim=;} value ; label llc ; Limited liability company jsc ; Joint-stock company sole ; Sole proprietorship === === form-field {#entity-type label="Entity type" type=select required options=#entity-types} === ## Compliance {#compliance} === form-note {#coc-note} Read the [code of conduct](https://example.invalid/coc) first. Ticking the box accepts its **anti-bribery** clauses. === === form-field {#agree label="I have read and accept the code of conduct" type=boolean required description=#coc-note} === ## Contacts {#contacts-heading} ==== form-group {#contacts label="Contacts" required description="At least one."} === form-field {#name label="Name" type=text required} === === form-field {#email label="Email" type=text pattern="^[^@]+@[^@]+$" required} === ==== ===== %% addresses: #vendor#legal-name, #vendor#contacts#email, #vendor#entity-types, #vendor#coc-note %% every form-* id is unique within the form's scope; the heading #company is not form-*, so document-level %% a form-field attribute value beginning with # names a form-* block of the enclosing form: options=, label=, description=, placeholder=
Read the code of conduct first. Ticking the box accepts its anti-bribery clauses.
| Type | Where | Body | id | Role |
|---|---|---|---|---|
| form | anywhere in a document | flow: form-* children, headings, prose | document-level | the container; handler= |
| form-field | in a form or a form-group | empty; non-empty warns | in scope, #vendor#email | one field; all its text in attributes |
| form-group | directly in the form; does not nest | form-fields only | in scope, and opens a scope | a repeating set of fields; required = at least one |
| form-options | directly in the form | a table body: format delim src as on table | in scope, #vendor#entity-types | an option list, consumed only by options= in the same form; never rendered alone |
| form-note | directly in the form | flow | in scope, #vendor#coc-note | rich text pointed at by a field's label= / description= / placeholder=; shareable |
options= pointing at a table is an error. A table in the document stays a table, and a chart cannot bind to an option list.Before: one table type meant a data table outside a form and an option list inside a field.description=#coc-note, a checked reference; the renderer can hang it as aria-describedby. The same direction as options=.Before: only unbound prose above the field, invisible to geml get and to screen readers.=.geml list shows at a glance which blocks belong to a form; a form-field body is always empty.Before: field / table / text blurred into the core types of the same name.-, since form- is now the specification's. Existing profile names such as style-rule are unaffected — style is not a registered type.geml get '#vendor' fetches it whole. The price: a form-field's # attributes resolve relative to the enclosing form, and reusing an option list across forms means one shared src= file.options=#id at a form-options, and label= description= placeholder=, when they begin with #, at a form-note. These are GEML's only relative references, confined to form-field's attributes. The test has precedent: a src= value beginning with # is a reference, anything else a path.form-note, parallel to the core note block, so it cannot be mistaken for text.Two things that look alike and are not. form-group replaced an earlier type=group, and it repeats by nature.
multiple on a scalar field. Ant Design: Select mode="tags".=== form-field {#tags label="Tags" type=text multiple description="Several allowed; press Enter after each."} ===
Form.List. A group is a shape of structure, not of value, hence form-group rather than type=group; it repeats by nature, so no multiple. type= is back to seven pure value shapes.==== form-group {#contacts label="Contacts" required description="At least one; add as many as needed."} === form-field {#name label="Name" type=text required} === === form-field {#email label="Email" type=text pattern="^[^@]+@[^@]+$" required} === === form-field {#role label="Role" type=select options=#roles} === ==== === form-options {#roles format=csv delim=;} value ; label biz ; Business tech ; Technical fin ; Finance === %% addresses: #vendor#contacts#email, #vendor#roles %% form-options sit directly in the form; options=#roles resolves in the enclosing form, so any group's field may use it
form-group is the repeating group: zero or more entries, each the set of its children's values; required means at least one. It sits directly in the form, holds only form-fields, and opens one scope. multiple is for scalar fields only. A non-repeating composite is not defined: visual grouping is a heading's job.=, and spares the parser and every downstream tool a recursive scope. The cost recorded under the proposal's Drawbacks 1 — a non-flat id space — is thereby bounded. It waits for a first instance.form-optionsThe contract is unchanged; the carrier became a type of its own. A type=select value must fall in the option list's value column.
=== form-options {#states format=csv delim=;} value ; label draft ; Draft accepted ; Accepted final ; Final === === form-field {#state label="State" type=select required options=#states} === %% what is submitted is the value `accepted`, not the label "Accepted" %% options= pointing at a table rather than a form-options: error
multiple it is a multi-select, and option labels may carry links SettledA form-options body parses its cells inline like a table, so a label column may hold a link. The 44 space-carrying options of the next.js form fit too.=== form-options {#categories-list format=csv delim=;} value ; label raw ; [Raw materials](https://example.invalid/cat/raw) — metals, polymers, textiles parts ; [Components](https://example.invalid/cat/parts) — machined, moulded, PCB contract-mfg ; Contract manufacturing logistics ; Logistics and warehousing === === form-field {#categories label="Supply categories" type=select multiple required options=#categories-list} ===
label column the text shown, further columns pass to the handler unread. src= brings a long list in from a CSV file; two forms wanting the same list point at the same file. A form-options is consumed only by options= in its own form and never rendered alone; one nothing points at earns a warning.type is back to seven pure value shapes. Constraint keys come from the profile: declared, never evaluated.
| type | value shape | renders as | count | note |
|---|---|---|---|---|
| text | any string, one line | input | 11 | path, url, email and tel all fold in; format differences via pattern= |
| textarea | string, several paragraphs | multi-line input | 7 | |
| number | a number | numeric input | 1 | range via min / max / step |
| date | a date | date picker | 1 | time and datetime wait for an instance |
| boolean | true / false | checkbox or switch | 1 | |
| select | one value of a form-options' value column; several with multiple | dropdown / radio group / checkbox group | 4 | includes the 44-option next.js dropdown |
| file | a file | upload | 0 | the only one kept on value shape rather than measurement |
geml-form/v1 profile; without the declaration they are unknown-attribute warnings. The processor stores the string; the handler enforces it.=== meta profile = "geml-form/v1" === === form-field {#revenue label="Annual revenue (CNY, millions)" type=number min=0 max=99999 step=1 description="Whole millions; blank if unaudited."} === === form-field {#phone label="Mobile" type=text pattern="^1\d{10}$" required description="11 digits"} === === form-field {#contract-end label="Contract end" type=date required description="Must be after the start date."} === %% "after the start date" is a cross-field rule: the document can only say it to a reader; only the handler checks it
| Constraint | Can the document carry it? | Who enforces |
|---|---|---|
| pattern · min · max · step · maxlength · accept | Yes, as profile attributes. The document stores strings and never evaluates them. | handler; a stylesheet may show them as hints |
| type=number · date · select · file | Yes. The type itself is a shape constraint. | handler |
| end after start · A excludes B · Y required only when X is ticked | No. §9.1: no expressions, no conditionals. Write it in the description for the reader. | handler only |
type= value SettledDegrades to text with a warning, unknown-field-type; the value is kept.=== form-field {#brand-color label="Brand colour" type=color} ===
Short text in attributes, long text in a form-note, the form-field body always empty.
=== form-field {#legal-name label="Legal name" description="Exactly as on the business licence." type=text required} === %% a non-empty body: warning form-field-has-body, not rendered
form-note SettledThe form-note sits in the form, outside the field, with no for=; the field's description= beginning with # points at it, and the renderer hangs it as aria-describedby. The next.js form's five-paragraph, fifteen-link descriptions fit.=== form-note {#coc-note} Read the [code of conduct](https://example.invalid/coc) first. Ticking the box accepts its **anti-bribery** and **data protection** clauses. === === form-field {#agree label="I have read and accept the code of conduct" type=boolean required description=#coc-note} === %% #coc-note resolves in the enclosing form, i.e. #vendor#coc-note; several fields may point at one form-note %% pointing at something other than a form-note: error; a form-note nothing points at: warning
Read the code of conduct first.
Ticking the box accepts its anti-bribery and data protection clauses.
label= description= placeholder= share one rule: the value is plain text, or, beginning with #, points at a form-note in the same form. Plain is one line of text; a form-note is flow content with links, bold, several paragraphs. A label pointing at a form-note renders inline; a placeholder pointing at one is flattened to text, since an input's placeholder cannot hold markup.form-note.value= on a multi-select names the single preselected option SettledAttribute values have no arrays. Preselecting several is the handler's default-value logic.=== form-field {#plan label="Settlement plan" type=select multiple value=net30 options=#plans} ===
#vendor form L7-30 #contacts-heading heading h2 L8-29 ← document-level id, not #vendor#contacts-heading #contact-name … ← per the proposal, #vendor#contact-name
A heading inside a form is an ordinary document heading: its id is document-level, untouched by field scoping, and it is not a field. Its section ends at the enclosing form or form-group fence, or at the next heading of the same or higher level. Visual grouping uses it; form-group is not for that.
= too short Settled · errorAny form-* block outside a form is an error, form-child-outside-form, whose message points at the likely short fence.=== form {#vendor handler=onboarding} Company === form-field {#legal-name label="Legal name" type=text required} ← same length as the form: closes it === ← opens a new anonymous block
==== form {#vendor handler=onboarding} Company === form-field {#legal-name label="Legal name" type=text required} === ====
geml check exits non-zero; the document does not pass.| form-child-outside-form | error | a form-* block outside a form |
| form-field-has-body | warning | a non-empty form-field body, not rendered |
| unknown-field-type | warning | a type outside the seven; rendered as text |
| options-not-form-options | error | options= names something other than a form-options in this form |
| note-not-form-note | error | a # in label / description / placeholder names something other than a form-note in this form |
| unused-form-block | warning | a form-options or form-note no field points at |
| duplicate-id | error | two ids the same within one scope |
GEML spec 1.0. Hyphenated type names already parse as block types — the profile's style-rule is an existing case.
| # | Input | Result | What it showed |
|---|---|---|---|
| 1 | A form written as the first draft spelled it, ending with [[#vendor#legal-name]] | warning unknown block type formerror unresolved reference | An unregistered form is only a warning; a field reference is an error, so #a#b resolution is the first thing to implement. |
| 2 | The same document with form / field replaced by the registered note | 0 errors 29 fields and 4 option tables all addressable | Nesting holds under the existing fence rules; what is missing is the type names and the scoping rule. |
| 3 | note nested four deep, with a heading in the body and an empty body | ok no diagnostics | Group nesting, headings in a body and empty bodies all parse already; the family uses at most three levels. |