@orkestrel/scaffold 0.0.103 → 0.0.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +84 -1
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +21 -6
- package/dist/agents/skills/orkestrel-harden/scripts/titles.js +338 -0
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +3 -39
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +433 -47
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +2 -1
- package/dist/bin/main.js +16 -9
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/orchestration.md +3 -3
- package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +2 -2
- package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
- package/dist/host/agents/skills/orkestrel-build/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +100 -1
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +27 -6
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +4 -4
- package/dist/host/agents/skills/orkestrel-falsify/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +8 -7
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +6 -6
- package/dist/host/agents/skills/orkestrel-harden/SKILL.md +6 -1
- package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +1700 -0
- package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -2
- package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +48 -0
- package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +4 -2
- package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +425 -0
- package/dist/host/agents/skills/orkestrel-journey/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-journey/references/captures.md +9 -8
- package/dist/host/agents/skills/orkestrel-polish/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +1 -0
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +56 -13
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +6 -6
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +5 -49
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +646 -49
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +2 -1
- package/dist/host/agents/transports/codex.md +1 -1
- package/dist/host/claude/agents/orkestrel.md +54 -53
- package/dist/host/claude/agents/reviewer.md +4 -4
- package/dist/host/claude/rules/documentation.md +2 -1
- package/dist/host/claude/rules/patterns.md +1 -1
- package/dist/host/claude/rules/quality.md +2 -2
- package/dist/host/claude/rules/styles.md +1 -1
- package/dist/host/claude/rules/tests.md +34 -8
- package/dist/host/claude/rules/typescript.md +29 -0
- package/dist/host/claude/rules/workspace.md +32 -22
- package/dist/host/claude/rules/writing.md +34 -26
- package/dist/host/claude/settings.json +2 -0
- package/dist/host/claude/skills/orkestrel-debrief/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +1 -1
- package/dist/host/codex/agents/analyst.toml +3 -3
- package/dist/host/codex/agents/reviewer.toml +4 -4
- package/dist/host/configs/policy.ts +13 -2
- package/dist/host/dotfiles/editorconfig +0 -1
- package/dist/host/dotfiles/gitattributes +0 -1
- package/dist/host/dotfiles/gitignore +0 -1
- package/dist/host/dotfiles/oxlintrc.json +24 -0
- package/dist/host/guides/agent.md +1638 -598
- package/dist/host/guides/budget.md +89 -29
- package/dist/host/guides/form.md +477 -91
- package/dist/host/guides/mcp.md +14 -2
- package/dist/host/guides/ollama.md +97 -73
- package/dist/host/guides/probe.md +1 -1
- package/dist/host/guides/scaffold.md +158 -101
- package/dist/host/guides/study.md +2635 -0
- package/dist/host/guides/table.md +225 -15
- package/dist/host/guides/terminal.md +22 -20
- package/dist/host/guides/test.md +689 -255
- package/dist/host/manifest.json +87 -63
- package/dist/host/tests/config.test.ts +136 -82
- package/dist/host/tests/policy.test.ts +39 -1
- package/dist/host/tests/setupPolicy.ts +86 -28
- package/dist/src/core/index.cjs +1387 -611
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +35 -20
- package/dist/src/core/index.d.ts +35 -20
- package/dist/src/core/index.js +1385 -612
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +12 -12
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +12 -12
- package/dist/src/server/index.d.ts +12 -12
- package/dist/src/server/index.js +12 -12
- package/dist/src/server/index.js.map +1 -1
- package/package.json +4 -4
package/dist/host/guides/form.md
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
> The environment-agnostic form document: a `FormSchema` stating what is asked, a `Form` holding
|
|
4
4
|
> the answers given against it, declarative `FieldRule` data stating what those answers must
|
|
5
|
-
> satisfy, and
|
|
5
|
+
> satisfy, acknowledged editing, and a terminal submit that settles the form exactly once.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Terminal prompts and editable forms share questions, answers, and validation. An editable form
|
|
8
|
+
prepares a snapshot and accepts an acknowledgement while further edits remain possible. A terminal
|
|
9
|
+
form settles through `submit`. Each host contributes the part it owns. Parking is the server environment's
|
|
10
10
|
contribution: `answer` is a form whose result nobody has resolved yet, so a server can hand the
|
|
11
11
|
document out, wait, and receive the answers back through the same promise a local caller awaits.
|
|
12
|
-
Rendering
|
|
13
|
-
|
|
12
|
+
Rendering belongs to the browser entry. The core never reads a keyboard or opens a socket;
|
|
13
|
+
the browser binding projects that same document into native controls.
|
|
14
14
|
|
|
15
15
|
The core is pure and total. Every guard returns `false` off-shape rather than throwing, every parser
|
|
16
16
|
returns `undefined` on refusal, and every value the form hands back is a frozen owned copy.
|
|
@@ -19,9 +19,9 @@ throw escapes the mutation call unchanged.
|
|
|
19
19
|
|
|
20
20
|
## Surface
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
The document exports come from `@orkestrel/form` ([`src/core`](../src/core)); the native binding
|
|
23
|
+
exports come from `@orkestrel/form/browser` ([`src/browser`](../src/browser)). Every declaration
|
|
24
|
+
is reachable through its environment's barrel.
|
|
25
25
|
|
|
26
26
|
### Open a form, answer it, and settle it
|
|
27
27
|
|
|
@@ -54,26 +54,27 @@ optional member and `plus` introducing its call-signature members, and a type al
|
|
|
54
54
|
literal with a union's arms escaped as `\|`. An extended interface's name comes before `plus`,
|
|
55
55
|
with the members it adds after.
|
|
56
56
|
|
|
57
|
-
| API | Kind | Shape
|
|
58
|
-
| --------------- | --------- |
|
|
59
|
-
| `FormSchema` | interface | `{ name?, label?, help?, groups?, fields }`
|
|
60
|
-
| `FormGroup` | interface | `{ name, label, help? }`
|
|
61
|
-
| `FormField` | type | `TextField \| EditorField \| PasswordField \| NumberField \| DateField \| TimeField \| DatetimeField \| ColorField \| ConfirmField \| SelectField \| CheckboxField \| FileField` | Represents any field a schema can declare. |
|
|
62
|
-
| `FieldBase` | interface | `{ name, label?, help?, group?, hidden?, disabled?, locked?, rule?, meta? }`
|
|
63
|
-
| `FieldControl` | type | `'text' \| 'editor' \| 'password' \| 'number' \| 'date' \| 'time' \| 'datetime' \| 'color' \| 'confirm' \| 'select' \| 'checkbox' \| 'file'`
|
|
64
|
-
| `FieldChoice` | interface | `{ value, label, help?, disabled? }`
|
|
65
|
-
| `TextField` | interface | `FieldBase plus { control, default?, placeholder? }`
|
|
66
|
-
| `EditorField` | interface | `FieldBase plus { control, default?, placeholder? }`
|
|
67
|
-
| `PasswordField` | interface | `FieldBase plus { control, mask? }`
|
|
68
|
-
| `NumberField` | interface | `FieldBase plus { control, default?, placeholder? }`
|
|
69
|
-
| `DateField` | interface | `FieldBase plus { control, default? }`
|
|
70
|
-
| `TimeField` | interface | `FieldBase plus { control, default? }`
|
|
71
|
-
| `DatetimeField` | interface | `FieldBase plus { control, default? }`
|
|
72
|
-
| `ColorField` | interface | `FieldBase plus { control, default? }`
|
|
73
|
-
| `ConfirmField` | interface | `FieldBase plus { control, default? }`
|
|
74
|
-
| `SelectField` | interface | `FieldBase plus { control, choices, default?, open? }`
|
|
75
|
-
| `CheckboxField` | interface | `FieldBase plus { control, choices, default? }`
|
|
76
|
-
| `
|
|
57
|
+
| API | Kind | Shape | Summary |
|
|
58
|
+
| --------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
59
|
+
| `FormSchema` | interface | `{ name?, label?, help?, groups?, fields }` | Describes everything a form asks. |
|
|
60
|
+
| `FormGroup` | interface | `{ name, label, help? }` | Represents a named section of a form. |
|
|
61
|
+
| `FormField` | type | `TextField \| EditorField \| PasswordField \| NumberField \| DateField \| TimeField \| DatetimeField \| ColorField \| ConfirmField \| SelectField \| CheckboxField \| ListField \| FileField` | Represents any field a schema can declare. |
|
|
62
|
+
| `FieldBase` | interface | `{ name, label?, help?, group?, hidden?, disabled?, locked?, rule?, meta? }` | Declares what every field carries, whatever its control. |
|
|
63
|
+
| `FieldControl` | type | `'text' \| 'editor' \| 'password' \| 'number' \| 'date' \| 'time' \| 'datetime' \| 'color' \| 'confirm' \| 'select' \| 'checkbox' \| 'list' \| 'file'` | Names the control a field presents to the person answering it. |
|
|
64
|
+
| `FieldChoice` | interface | `{ value, label, help?, disabled? }` | Represents one option a `select` or `checkbox` field offers. |
|
|
65
|
+
| `TextField` | interface | `FieldBase plus { control, default?, placeholder? }` | Represents a single line of text. |
|
|
66
|
+
| `EditorField` | interface | `FieldBase plus { control, default?, placeholder? }` | Represents text over many lines. |
|
|
67
|
+
| `PasswordField` | interface | `FieldBase plus { control, mask? }` | Represents a secret, obscured as it is typed. |
|
|
68
|
+
| `NumberField` | interface | `FieldBase plus { control, default?, placeholder? }` | Represents a number. |
|
|
69
|
+
| `DateField` | interface | `FieldBase plus { control, default? }` | Represents a calendar date, held as the control's own `YYYY-MM-DD` string. |
|
|
70
|
+
| `TimeField` | interface | `FieldBase plus { control, default? }` | Represents a time of day, held as the control's own `HH:MM` string, with seconds optional. |
|
|
71
|
+
| `DatetimeField` | interface | `FieldBase plus { control, default? }` | Represents a date and a time of day together, with no zone, held as the control's own string. |
|
|
72
|
+
| `ColorField` | interface | `FieldBase plus { control, default? }` | Represents a color, held as the control's own six-digit `#rrggbb` string. |
|
|
73
|
+
| `ConfirmField` | interface | `FieldBase plus { control, default? }` | Represents a single on/off box, holding a boolean. |
|
|
74
|
+
| `SelectField` | interface | `FieldBase plus { control, choices, default?, open? }` | Represents one choice out of a list. |
|
|
75
|
+
| `CheckboxField` | interface | `FieldBase plus { control, choices, default? }` | Represents any number of choices out of a list, holding the checked values. |
|
|
76
|
+
| `ListField` | interface | `FieldBase plus { control, default? }` | Represents an ordered list of free-text strings. |
|
|
77
|
+
| `FileField` | interface | `FieldBase plus { control, accept?, multiple? }` | Represents one or more files, by name. |
|
|
77
78
|
|
|
78
79
|
### Answers and rules
|
|
79
80
|
|
|
@@ -103,18 +104,20 @@ literal with a union's arms escaped as `\|`. A function row's `Shape` cell holds
|
|
|
103
104
|
and a guard row's the type it narrows to. A class row's `Shape` cell holds the interface it
|
|
104
105
|
implements, or its constructor signature where it implements none.
|
|
105
106
|
|
|
106
|
-
| API
|
|
107
|
-
|
|
|
108
|
-
| `Form`
|
|
109
|
-
| `FormInterface`
|
|
110
|
-
| `createForm`
|
|
111
|
-
| `FormOptions`
|
|
112
|
-
| `FormStatus`
|
|
113
|
-
| `FormResult`
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
107
|
+
| API | Kind | Shape | Summary |
|
|
108
|
+
| ----------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
109
|
+
| `Form` | class | `FormInterface` | Implements `FormInterface` exactly, over an owned schema, the answers given against it, and the errors they carry. |
|
|
110
|
+
| `FormInterface` | interface | `{ emitter, schema, values, baseline, pending, errors, touched, disabled, status, valid, dirty, answer } plus field, fill, touch, invalidate, disable, enable, submit, prepare, accept, abort, clear, destroy` | Declares the contract a form exposes: the state it holds and the calls that move it. |
|
|
111
|
+
| `createForm` | function | `(schema: FormSchema, options?: FormOptions) => FormInterface` | Opens a form against a schema. |
|
|
112
|
+
| `FormOptions` | interface | `{ on?, error?, values?, messages? }` | Describes how to open a form. |
|
|
113
|
+
| `FormStatus` | type | `'editing' \| 'settled' \| 'abandoned'` | Represents where a form sits in its life. |
|
|
114
|
+
| `FormResult` | type | `Result<FormValues, readonly FieldError[]>` | Reports what a submit answers with: the values, or every error that stopped them. |
|
|
115
|
+
| `FormSubmission` | interface | `{ values, signal }` | Carries an owned, frozen answer snapshot and the signal for its pending acknowledgement. |
|
|
116
|
+
| `FormPreparationResult` | type | `Result<FormSubmission, readonly FieldError[]>` | Reports a prepared submission, or every error that prevented its preparation. |
|
|
117
|
+
| `FormEventMap` | type | `{ fill, touch, validate, disable, enable, submit, prepare, accept, abort, clear, abandon }` | Lists everything a form announces. |
|
|
118
|
+
| `FormError` | class | `new (code: FormErrorCode, message: string, context?: JSONRecord) => FormError` | Represents an error raised by the form domain. |
|
|
119
|
+
| `FormErrorCode` | type | `'SCHEMA' \| 'FIELD' \| 'CONTROL' \| 'SETTLED' \| 'ABANDONED' \| 'PENDING'` | Names the machine-readable code a form error carries. |
|
|
120
|
+
| `isFormError` | function | `FormError` | Determines whether an unknown value is a form error. |
|
|
118
121
|
|
|
119
122
|
`FormInterface`'s readonly data members are the names in its `Shape` cell before `plus`, and they
|
|
120
123
|
stay here rather than in `## Methods`; the call-signature members after `plus` are documented under
|
|
@@ -229,6 +232,47 @@ owned value rather than the caller's.
|
|
|
229
232
|
| `parseValue` | function | Parses one answer against its field control. |
|
|
230
233
|
| `parseValues` | function | Parses a strict answer record against the fields declared by a schema. |
|
|
231
234
|
|
|
235
|
+
### Native browser binding
|
|
236
|
+
|
|
237
|
+
The browser entry publishes the controlled native form and its host readers.
|
|
238
|
+
|
|
239
|
+
| Symbol | Kind | Shape | Summary |
|
|
240
|
+
| ---------------------- | --------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
241
|
+
| `FormControl` | type | `HTMLInputElement` or `HTMLSelectElement` or `HTMLTextAreaElement` | Represents a native answer control admitted by the binding. |
|
|
242
|
+
| `FormBindingErrorCode` | type | `'CONTROL'` or `'PENDING'` or `'DOM'` | Names a native binding refusal. |
|
|
243
|
+
| `FormBindingEventMap` | type | `{ render, write, submit, error }` | Lists the native binding's notifications after their associated DOM effects. |
|
|
244
|
+
| `FormBindingOptions` | interface | `{ feedback?, summary?, presentations?, defer?, on?, error? }` | Configures neutral feedback destinations and isolated event listeners. |
|
|
245
|
+
| `FormBindingInterface` | interface | `{ element, document, destroyed, emitter } plus read, refresh, destroy` | Declares a native form's borrowed document and synchronization lifecycle. |
|
|
246
|
+
| `FormBindingAttribute` | interface | `{ original, value }` | Records an attribute's authored value and the binding's last contribution. |
|
|
247
|
+
| `FormBindingValidity` | interface | `{ original, value }` | Records the binding's custom-validity contribution beside the authored message. |
|
|
248
|
+
| `FormBinding` | class | `FormBindingInterface` | Implements a controlled native form over a borrowed document. |
|
|
249
|
+
| `FormBindingError` | class | `{ code, context }` | Reports an incompatible control, overlapping submission, or native DOM failure. |
|
|
250
|
+
| `createFormBinding` | function | `(element, document, options?) => FormBindingInterface` | Creates a native form binding exposed through its lifecycle interface. |
|
|
251
|
+
| `isFormBindingError` | function | `(value: unknown) => value is FormBindingError` | Checks whether a value is a native binding refusal. |
|
|
252
|
+
| `isFormControl` | function | `(value: unknown) => value is FormControl` | Checks whether a value is a native answer control. |
|
|
253
|
+
| `matchesFormControls` | function | `(field, controls) => boolean` | Checks a native control population against a field's declared value shape. |
|
|
254
|
+
| `readNativeMessages` | function | `(control, errors) => readonly string[]` | Reads native constraint messages not already reported by document rules. |
|
|
255
|
+
| `readFormControls` | function | `(field, controls) => FieldValue or undefined` | Reads one compatible native population using the document's blank-to-absence semantics. |
|
|
256
|
+
|
|
257
|
+
The standalone reader uses the same admission rules as the binding.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import {
|
|
261
|
+
FormBindingError,
|
|
262
|
+
isFormBindingError,
|
|
263
|
+
isFormControl,
|
|
264
|
+
matchesFormControls,
|
|
265
|
+
readFormControls,
|
|
266
|
+
} from '@orkestrel/form/browser'
|
|
267
|
+
|
|
268
|
+
const input = document.createElement('input')
|
|
269
|
+
input.value = 'Ada'
|
|
270
|
+
isFormControl(input) // true
|
|
271
|
+
matchesFormControls({ control: 'text', name: 'name' }, [input]) // true
|
|
272
|
+
readFormControls({ control: 'text', name: 'name' }, [input]) // 'Ada'
|
|
273
|
+
isFormBindingError(new FormBindingError('CONTROL', 'Duplicate controls')) // true
|
|
274
|
+
```
|
|
275
|
+
|
|
232
276
|
## Controls
|
|
233
277
|
|
|
234
278
|
Each control fixes both the options its field accepts and the `FieldValue` it holds. The `confirm`,
|
|
@@ -453,6 +497,41 @@ const topics: CheckboxField = {
|
|
|
453
497
|
}
|
|
454
498
|
```
|
|
455
499
|
|
|
500
|
+
### list
|
|
501
|
+
|
|
502
|
+
`ListField` holds an ordered array of free-text strings, including duplicates. `required` rejects
|
|
503
|
+
absence and an empty array. `minimum` and `maximum` are nonnegative integer entry counts; the
|
|
504
|
+
minimum cannot exceed `LIST_LIMIT`, and a required list cannot have a zero maximum. String rules
|
|
505
|
+
check each entry. Their messages prefix the failing value, quoted with JSON escaping, and retain
|
|
506
|
+
the rule name for native-message deduplication. Failures appear in rule order, then entry order.
|
|
507
|
+
`custom` runs once against the whole array, after the named rules.
|
|
508
|
+
|
|
509
|
+
Declare a recipient list with an email constraint.
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
import type { ListField } from '@orkestrel/form'
|
|
513
|
+
|
|
514
|
+
const to: ListField = {
|
|
515
|
+
control: 'list',
|
|
516
|
+
name: 'to',
|
|
517
|
+
label: 'To',
|
|
518
|
+
rule: { required: true, maximum: 50, email: true },
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
A browser list binds to one native `select multiple`. The page can add and remove options and
|
|
523
|
+
dispatch native input or change events to import the selection. Document fills create missing
|
|
524
|
+
options and order selected options to match the array, preserving repeated values. Disabled options
|
|
525
|
+
and options in disabled groups do not contribute an answer. Each selected enabled value posts under
|
|
526
|
+
the select's name in FormData. The closed `checkbox` field retains its declared vocabulary.
|
|
527
|
+
|
|
528
|
+
Native reset restores the list's options, optgroups, and selection captured before its first
|
|
529
|
+
projection, including authored options the page removed. It imports that restored selection into
|
|
530
|
+
the document after clearing touch and pending work. For lists this is the authored selection even
|
|
531
|
+
after acknowledgement; it can therefore differ from the acknowledged baseline and leave `dirty`
|
|
532
|
+
true. A direct document `clear()` retains its baseline semantics. Other fields retain their
|
|
533
|
+
acknowledged-reset behavior. Destroying the binding retains the live options and selections.
|
|
534
|
+
|
|
456
535
|
### file
|
|
457
536
|
|
|
458
537
|
A file value is a list of names. `multiple` admits more than one, and without it a second name is
|
|
@@ -542,8 +621,10 @@ Object.getPrototypeOf(form.field('email')?.meta ?? {}) // null
|
|
|
542
621
|
|
|
543
622
|
A renderer picks an affordance from what a field asks a person for, not from the name its control
|
|
544
623
|
carries. The categories that decision is made in are the portable ones the `enterprise-bootstrap`
|
|
545
|
-
skill catalogs in its `references/inputs.md` file
|
|
546
|
-
|
|
624
|
+
skill catalogs in its `references/inputs.md` file. Resolve that skill through this repository's
|
|
625
|
+
Scaffold pointer: `../scaffold/.agents/skills/enterprise-bootstrap/references/inputs.md` beside a
|
|
626
|
+
checkout, otherwise `node_modules/@orkestrel/scaffold/dist/host/agents/skills/enterprise-bootstrap/references/inputs.md`.
|
|
627
|
+
The category names in the
|
|
547
628
|
table are that catalog's own, and a category name is the join key: read the row here, then open the
|
|
548
629
|
section carrying that name there.
|
|
549
630
|
|
|
@@ -576,9 +657,8 @@ invents a control:
|
|
|
576
657
|
admit a value the list does not offer. `choices` is data in the schema, so the search field, the
|
|
577
658
|
ranking, and the menu belong to the renderer, and a list fetched while somebody types is the host
|
|
578
659
|
building a new schema.
|
|
579
|
-
- **An ordered set of tags** — `checkbox` over a fixed vocabulary,
|
|
580
|
-
|
|
581
|
-
value only, and no control holds a list of unlisted ones.
|
|
660
|
+
- **An ordered set of tags** — `checkbox` over a fixed vocabulary, or `list` for free-text entries.
|
|
661
|
+
Both keep the order of the filled array; `list` also retains duplicates.
|
|
582
662
|
- **A rating** — `select` over the fixed values, which is the radio group the catalog draws, or
|
|
583
663
|
`number` with `minimum`, `maximum`, and `step` where the scale is continuous.
|
|
584
664
|
- **A step in a sequence** — outside the document. A step indicator reports position rather than
|
|
@@ -590,18 +670,18 @@ A rule is data, not a closure. That is what lets a schema cross a wire and valid
|
|
|
590
670
|
side exactly as it validated here — with the single exception of `custom`, which is a function and
|
|
591
671
|
therefore does not travel.
|
|
592
672
|
|
|
593
|
-
| Rule | Operand | What it measures
|
|
594
|
-
| -------------- | ---------------- |
|
|
595
|
-
| `required` | `true` |
|
|
596
|
-
| `minimum` | number or string | Characters for text, editor, and password; magnitude for number; chronology for `date`, `time`, and `datetime`; selections for checkbox and file. |
|
|
597
|
-
| `maximum` | number or string | The same measure as `minimum`, at the other end.
|
|
598
|
-
| `step` | number | The interval a numeric value must land on, counted from `minimum` or from zero. Number only.
|
|
599
|
-
| `pattern` | string | Regular-expression source the whole value must match. String-valued controls
|
|
600
|
-
| `email` | `true` | That the whole value is an email address, per `EMAIL_PATTERN`.
|
|
601
|
-
| `url` | `true` | That the whole value is an absolute HTTP or HTTPS URL, per `URL_PATTERN`.
|
|
602
|
-
| `integer` | `true` | That a number has no fractional part, or that a string is a base-ten integer.
|
|
603
|
-
| `alphanumeric` | `true` | That the whole value is ASCII letters and digits, per `ALPHANUMERIC_PATTERN`.
|
|
604
|
-
| `custom` | `FieldValidator` | Anything the rest cannot say. It runs last, on an absent value too, and it is the only rule that sees the rest of the form.
|
|
673
|
+
| Rule | Operand | What it measures |
|
|
674
|
+
| -------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
675
|
+
| `required` | `true` | Requires an entry for `list`; requires a present answer for other controls, including `''`, `[]`, `false`, and `0`. |
|
|
676
|
+
| `minimum` | number or string | Characters for text, editor, and password; magnitude for number; chronology for `date`, `time`, and `datetime`; selections for checkbox, list, and file. |
|
|
677
|
+
| `maximum` | number or string | The same measure as `minimum`, at the other end. |
|
|
678
|
+
| `step` | number | The interval a numeric value must land on, counted from `minimum` or from zero. Number only. |
|
|
679
|
+
| `pattern` | string | Regular-expression source the whole value must match. String-valued controls and each list entry. |
|
|
680
|
+
| `email` | `true` | That the whole value or each list entry is an email address, per `EMAIL_PATTERN`. |
|
|
681
|
+
| `url` | `true` | That the whole value or each list entry is an absolute HTTP or HTTPS URL, per `URL_PATTERN`. |
|
|
682
|
+
| `integer` | `true` | That a number has no fractional part, or that a string or each list entry is a base-ten integer. |
|
|
683
|
+
| `alphanumeric` | `true` | That the whole value or each list entry is ASCII letters and digits, per `ALPHANUMERIC_PATTERN`. |
|
|
684
|
+
| `custom` | `FieldValidator` | Anything the rest cannot say. It runs last, on an absent value too, and it is the only rule that sees the rest of the form. |
|
|
605
685
|
|
|
606
686
|
The operand's type follows the control family. `minimum` and `maximum` take a number wherever the
|
|
607
687
|
measure is a count or a magnitude, and take a string written in the control's own format wherever
|
|
@@ -635,9 +715,9 @@ formatMessage('required', undefined, { required: 'We need this one' }) // 'We ne
|
|
|
635
715
|
### How an answer is counted
|
|
636
716
|
|
|
637
717
|
**`undefined` is the only absence.** A field is unanswered when `values` has no key for it, and
|
|
638
|
-
answered otherwise. `''`, a string of spaces, `[]`, `false`, and `0` are all answers
|
|
639
|
-
|
|
640
|
-
|
|
718
|
+
answered otherwise. `''`, a string of spaces, `[]`, `false`, and `0` are all answers. `required`
|
|
719
|
+
accepts every present answer except an empty `list`, which requires at least one entry. A length
|
|
720
|
+
rule is `minimum`, and a "must be ticked" rule is `custom`.
|
|
641
721
|
|
|
642
722
|
**This is not HTML's model, and the difference is deliberate.** A browser fails `required` on the
|
|
643
723
|
exact empty string, so an empty text input is unanswered there and answered here. Neither model is
|
|
@@ -717,10 +797,8 @@ evaluateField(vat, undefined, { account: 'business' })
|
|
|
717
797
|
evaluateField(vat, undefined, { account: 'personal' }) // []
|
|
718
798
|
```
|
|
719
799
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
takes several addresses is `text` plus a `custom` that splits the value and tests each part with the
|
|
723
|
-
exported `EMAIL_PATTERN` — the same pattern the `email` rule uses, so the two agree by construction.
|
|
800
|
+
Use a `list` with `email: true` for recipient arrays. A page retaining a comma-separated text field
|
|
801
|
+
can still use `custom` to split and check that scalar value with `EMAIL_PATTERN`.
|
|
724
802
|
|
|
725
803
|
```ts
|
|
726
804
|
import { evaluateField, EMAIL_PATTERN } from '@orkestrel/form'
|
|
@@ -866,7 +944,7 @@ group, a default its own control cannot hold, a rule on a control that cannot me
|
|
|
866
944
|
above its maximum, an uncompilable pattern, or a breach of any named budget. It also reports the
|
|
867
945
|
bounds no answer could satisfy: a required closed `select` with no enabled choice, a `checkbox` whose
|
|
868
946
|
positive `minimum` exceeds its enabled-choice count, and a negative `maximum` on a control that
|
|
869
|
-
measures a length or a count — `text`, `editor`, `password`, `checkbox`, or `file`. A required
|
|
947
|
+
measures a length or a count — `text`, `editor`, `password`, `checkbox`, `list`, or `file`. A required
|
|
870
948
|
`checkbox` alone remains satisfiable because `[]` is a present answer, `maximum: 0` on a `text` is
|
|
871
949
|
satisfiable by `''`, and a negative `maximum` on a `number` is an ordinary value bound.
|
|
872
950
|
|
|
@@ -945,7 +1023,8 @@ matchesField(when, '2026-13-01') // false — month 13 is not spelled correctly
|
|
|
945
1023
|
|
|
946
1024
|
A form opens `editing`, turns `settled` on its first valid submit, and turns `abandoned` when it is
|
|
947
1025
|
destroyed before settling. Both end states are terminal, and every write to a form in either one is
|
|
948
|
-
refused with a `FormError`.
|
|
1026
|
+
refused with a `FormError`. Callback-facing `accept`, `abort`, and receipt-aware `invalidate` return
|
|
1027
|
+
`false` for inactive receipts even after teardown. Every getter keeps answering afterwards.
|
|
949
1028
|
|
|
950
1029
|
A destroy requested while a mutation batch is open records the request, refuses every subsequent
|
|
951
1030
|
write from that instant, and defers teardown until the outermost batch closes. The batch's own
|
|
@@ -960,7 +1039,7 @@ previous error list in place. The "Errors are current after completed evaluation
|
|
|
960
1039
|
[Contract](#contract) states the exact partial-state boundary.
|
|
961
1040
|
|
|
962
1041
|
**`valid` and `dirty` are derived on read.** `valid` is true when `errors` is empty. `dirty` is true
|
|
963
|
-
|
|
1042
|
+
when the answers differ from `baseline`, the acknowledged answers. Neither is stored, so
|
|
964
1043
|
neither can drift.
|
|
965
1044
|
|
|
966
1045
|
**`touched` is the fields somebody has visited.** It is what lets a renderer withhold an error until
|
|
@@ -1181,13 +1260,13 @@ written, so a refused write changes nothing. Passing `undefined` clears one fiel
|
|
|
1181
1260
|
coupon already spent. One field holds one external failure, a second call replaces the first, and
|
|
1182
1261
|
the failure lasts until that field is filled again or the form is cleared.
|
|
1183
1262
|
|
|
1184
|
-
`baseline`
|
|
1185
|
-
|
|
1186
|
-
what `clear` returns to
|
|
1187
|
-
did — reads `extractChanges(form.values, form.baseline)` and gets the names.
|
|
1263
|
+
`baseline` starts with the schema's defaults overlaid with seeded `values`. Acceptance advances the
|
|
1264
|
+
captured enabled fields to their submitted values or absence. It is what `dirty` measures against
|
|
1265
|
+
and what `clear` returns to. Read `extractChanges(form.values, form.baseline)` for the changed names.
|
|
1188
1266
|
|
|
1189
1267
|
`clear` returns every answer to `baseline`. It also clears `touched`, every external failure, and
|
|
1190
|
-
every runtime `disable` or `enable
|
|
1268
|
+
every runtime `disable` or `enable`. Pending acknowledgement is aborted; the disabled state returns
|
|
1269
|
+
to the schema's declarations and the answers return to the acknowledged baseline.
|
|
1191
1270
|
|
|
1192
1271
|
```ts
|
|
1193
1272
|
import { createForm, extractChanges } from '@orkestrel/form'
|
|
@@ -1204,7 +1283,7 @@ const form = createForm({
|
|
|
1204
1283
|
],
|
|
1205
1284
|
})
|
|
1206
1285
|
|
|
1207
|
-
form.baseline // { plan: 'free' } — the answers
|
|
1286
|
+
form.baseline // { plan: 'free' } — the opening answers until a submission is accepted
|
|
1208
1287
|
|
|
1209
1288
|
form.fill('email', 'ada@example.com')
|
|
1210
1289
|
form.valid // true
|
|
@@ -1307,7 +1386,10 @@ form holds now.
|
|
|
1307
1386
|
itself settles it, and that settlement is what the outer call returns — one `submit` event, one
|
|
1308
1387
|
resolved `answer`, and no evaluation after it.
|
|
1309
1388
|
|
|
1310
|
-
### Retrying
|
|
1389
|
+
### Retrying terminal completion
|
|
1390
|
+
|
|
1391
|
+
This sequence applies when the host intends to finish the document terminally. For repeated saves
|
|
1392
|
+
that leave the document editable, use [Prepare and acknowledge repeated saves](#prepare-and-acknowledge-repeated-saves).
|
|
1311
1393
|
|
|
1312
1394
|
**`submit` is the commit, not the attempt.** It is the moment this document is finished with, which
|
|
1313
1395
|
is why it settles the form and why nothing may be written afterwards. A network request that can be
|
|
@@ -1364,6 +1446,283 @@ form.submit().success // true
|
|
|
1364
1446
|
form.status // 'settled'
|
|
1365
1447
|
```
|
|
1366
1448
|
|
|
1449
|
+
### Prepare and acknowledge repeated saves
|
|
1450
|
+
|
|
1451
|
+
Use `prepare` for an editable form. It checks an owned snapshot of enabled fields and marks those
|
|
1452
|
+
fields touched. Failure returns the checked errors without a receipt. Success installs `pending`
|
|
1453
|
+
before emitting `prepare`, and leaves `status` at `editing` and `answer` unresolved. A pending
|
|
1454
|
+
receipt refuses another `prepare` or terminal `submit` with `PENDING`; ordinary editing continues.
|
|
1455
|
+
|
|
1456
|
+
Pass the exact receipt to `accept` after persistence succeeds. Acceptance advances the baseline
|
|
1457
|
+
for its captured enabled names, including deleting an absent answer. Excluded fields retain their
|
|
1458
|
+
previous baseline. Live values, disabled state, touched fields, and errors stay intact. Receipt
|
|
1459
|
+
values and lists are owned and frozen; its native `signal` remains usable by a transport.
|
|
1460
|
+
|
|
1461
|
+
This example acknowledges A after B was typed, then saves B:
|
|
1462
|
+
|
|
1463
|
+
```ts
|
|
1464
|
+
import { createForm } from '@orkestrel/form'
|
|
1465
|
+
|
|
1466
|
+
const form = createForm({ fields: [{ control: 'text', name: 'name', default: 'Opening' }] })
|
|
1467
|
+
form.fill('name', 'A')
|
|
1468
|
+
const first = form.prepare()
|
|
1469
|
+
if (first.success) {
|
|
1470
|
+
form.fill('name', 'B')
|
|
1471
|
+
form.accept(first.value) // true
|
|
1472
|
+
form.baseline.name // 'A'
|
|
1473
|
+
form.values.name // 'B'
|
|
1474
|
+
form.dirty // true
|
|
1475
|
+
first.value.signal.aborted // false
|
|
1476
|
+
}
|
|
1477
|
+
const second = form.prepare()
|
|
1478
|
+
if (second.success) form.accept(second.value) // true
|
|
1479
|
+
form.dirty // false
|
|
1480
|
+
form.status // 'editing'
|
|
1481
|
+
```
|
|
1482
|
+
|
|
1483
|
+
On a refused save, apply field failures with `invalidate(name, message, receipt)` while the receipt
|
|
1484
|
+
is active, then call `abort(receipt)`. Ticketed invalidation returns `false` if the field was excluded
|
|
1485
|
+
or refilled after capture, including A-to-B-to-A edits and same-value refills. An unknown field
|
|
1486
|
+
still throws `FIELD`. Unticketed invalidation keeps its local behavior and returns `true`.
|
|
1487
|
+
|
|
1488
|
+
Abort consumes the active identity before aborting its signal and emitting `abort`. It leaves the
|
|
1489
|
+
baseline and live values intact. A copied, foreign, consumed, or reset receipt returns `false` from
|
|
1490
|
+
completion methods without events. Successful acceptance leaves the signal un-aborted.
|
|
1491
|
+
|
|
1492
|
+
The following refusal remains retryable:
|
|
1493
|
+
|
|
1494
|
+
```ts
|
|
1495
|
+
import { createForm } from '@orkestrel/form'
|
|
1496
|
+
|
|
1497
|
+
const form = createForm({ fields: [{ control: 'text', name: 'email' }] })
|
|
1498
|
+
form.fill('email', 'ada@example.com')
|
|
1499
|
+
const prepared = form.prepare()
|
|
1500
|
+
if (prepared.success) {
|
|
1501
|
+
form.invalidate('email', 'Already registered', prepared.value) // true
|
|
1502
|
+
form.abort(prepared.value) // true
|
|
1503
|
+
prepared.value.signal.aborted // true
|
|
1504
|
+
form.accept(prepared.value) // false
|
|
1505
|
+
}
|
|
1506
|
+
form.fill('email', 'grace@example.com')
|
|
1507
|
+
const retry = form.prepare()
|
|
1508
|
+
if (retry.success) form.accept(retry.value) // true
|
|
1509
|
+
form.dirty // false
|
|
1510
|
+
```
|
|
1511
|
+
|
|
1512
|
+
Preparation refuses nested `prepare` and `submit` during validation. A failed check retains its
|
|
1513
|
+
checked errors even when an observer repairs them. A passing check drains observer edits through
|
|
1514
|
+
one further check, then keeps that check's snapshot even if its notifications produce later edits.
|
|
1515
|
+
The live errors retain those later mutations. A throwing validator produces no usable receipt and
|
|
1516
|
+
doesn't advance the baseline. A `prepare` observer may fill, invalidate, accept, abort, or destroy;
|
|
1517
|
+
the returned receipt still describes the prepared snapshot, so check `pending` before starting
|
|
1518
|
+
delayed work. Acceptance and abortion consume before notification, allowing a listener to prepare
|
|
1519
|
+
a successor without an older operation clearing it.
|
|
1520
|
+
|
|
1521
|
+
`clear` aborts pending work, restores the acknowledged baseline, clears touch and external failures,
|
|
1522
|
+
and restores the schema's disabled declarations. If a listener clears while preparation is checking,
|
|
1523
|
+
that interrupted preparation throws `PENDING`. Destroy aborts pending work at the existing deferred
|
|
1524
|
+
outer-batch teardown boundary. Transport scheduling, retries, and persistence remain the caller's
|
|
1525
|
+
responsibility; use the receipt's signal where the transport supports cancellation.
|
|
1526
|
+
|
|
1527
|
+
## Native binding
|
|
1528
|
+
|
|
1529
|
+
The binding borrows one document. Construction paints its answers into controls, including controls
|
|
1530
|
+
associated through the native `form` attribute. Unknown names and submitters stay outside the schema.
|
|
1531
|
+
Missing controls keep their logical answers. Incompatible controls and duplicate scalar controls
|
|
1532
|
+
raise a `CONTROL` binding error before answers or native values change. Radio and checkbox groups
|
|
1533
|
+
are supported; a confirm field uses one checkbox. Native input and change events import answers,
|
|
1534
|
+
and focusout touches the field. Explicit `read()` and `refresh()` also rediscover membership.
|
|
1535
|
+
A mutation observer follows membership, association, and native constraint attributes without polling.
|
|
1536
|
+
During mutation-driven painting, a focused text input or textarea whose value changed since its last
|
|
1537
|
+
projection keeps that edit until the input event reads it. Explicit document fills and refreshes still
|
|
1538
|
+
project the document's answer. Replacement controls receive the document's value.
|
|
1539
|
+
|
|
1540
|
+
### Bind an editable native form
|
|
1541
|
+
|
|
1542
|
+
This executable browser example prepares a receipt, accepts its snapshot after another edit, and
|
|
1543
|
+
resets to the acknowledged answer. Its native composition is proved in
|
|
1544
|
+
[`tests/src/browser/integration.test.ts`](../tests/src/browser/integration.test.ts).
|
|
1545
|
+
|
|
1546
|
+
```ts
|
|
1547
|
+
import { createForm } from '@orkestrel/form'
|
|
1548
|
+
import { createFormBinding } from '@orkestrel/form/browser'
|
|
1549
|
+
|
|
1550
|
+
const element = document.createElement('form')
|
|
1551
|
+
element.innerHTML = '<input name="name"><button>Save</button>'
|
|
1552
|
+
document.body.append(element)
|
|
1553
|
+
const model = createForm({ fields: [{ name: 'name', control: 'text', default: 'Ada' }] })
|
|
1554
|
+
const binding = createFormBinding(element, model)
|
|
1555
|
+
const input = element.querySelector('input')
|
|
1556
|
+
if (input === null) throw new Error('Missing name control')
|
|
1557
|
+
input.value // 'Ada'
|
|
1558
|
+
input.value = 'Grace'
|
|
1559
|
+
binding.read()
|
|
1560
|
+
element.requestSubmit()
|
|
1561
|
+
const receipt = model.pending
|
|
1562
|
+
if (receipt === undefined) throw new Error('Submission refused')
|
|
1563
|
+
receipt.values.name // 'Grace'
|
|
1564
|
+
model.fill('name', 'Later edit')
|
|
1565
|
+
model.accept(receipt) // true
|
|
1566
|
+
model.dirty // true
|
|
1567
|
+
element.reset()
|
|
1568
|
+
input.value // 'Grace'
|
|
1569
|
+
model.dirty // false
|
|
1570
|
+
binding.refresh()
|
|
1571
|
+
binding.destroy()
|
|
1572
|
+
model.status // 'editing'
|
|
1573
|
+
model.destroy()
|
|
1574
|
+
element.remove()
|
|
1575
|
+
```
|
|
1576
|
+
|
|
1577
|
+
The binding owns native `noValidate` during its lifetime. An admitted submit prevents navigation,
|
|
1578
|
+
imports controls, touches enabled fields, checks native validity, and prepares the document. Its
|
|
1579
|
+
`submit` event carries the active receipt and the native submitter, or `undefined` without one.
|
|
1580
|
+
A receipt consumed during a core prepare listener is never dispatched to persistence. The consumer
|
|
1581
|
+
alone persists, accepts, or aborts it. A duplicate submission raises the binding's `PENDING` domain
|
|
1582
|
+
error, creates no receipt, and leaves edits enabled. The form and submit actions carry `aria-busy`;
|
|
1583
|
+
their dimensions and enabled state stay native. A prevented submit or reset has no binding effect.
|
|
1584
|
+
An admitted reset calls the document's clear operation and prevents HTML default-value restoration.
|
|
1585
|
+
List controls then restore their captured authored options and selection as described under
|
|
1586
|
+
[List fields](#list).
|
|
1587
|
+
|
|
1588
|
+
Neutral feedback uses owned spans carrying `data-form-feedback="field"`, each with a unique ID.
|
|
1589
|
+
Supply `feedback: new Map([['field', destination]])` to choose a parent; authored children remain.
|
|
1590
|
+
Otherwise the binding places the span after the field's last native control. Touched errors set
|
|
1591
|
+
`aria-invalid` and add the feedback ID to authored `aria-describedby` tokens. An optional `summary`
|
|
1592
|
+
destination receives an owned `ul[data-form-summary]`; its links focus eligible controls, and errors
|
|
1593
|
+
without reachable controls remain text. An invalid submission focuses the first eligible invalid
|
|
1594
|
+
control. The binding never invokes a native validity popup. A `render` event follows DOM effects,
|
|
1595
|
+
including effects triggered by core changes, so a presentation adapter can apply its own classes.
|
|
1596
|
+
|
|
1597
|
+
Native and document validation remain distinct sources. A named rule's message replaces feedback
|
|
1598
|
+
for that native constraint. Required, bounds, length, step, pattern, email, and URL failures match
|
|
1599
|
+
by rule identity, never localized text. A lone uncovered failure keeps the platform's message.
|
|
1600
|
+
Competing native failures receive separate constraint-specific sentences, so an uncovered failure
|
|
1601
|
+
cannot repeat a covered rule's sentence. Authored custom validity retains its exact text, and bad
|
|
1602
|
+
input retains a failure message. Native step feedback does not assume a zero step base.
|
|
1603
|
+
Native validity still refuses preparation and never overwrites the document's external invalidation slot.
|
|
1604
|
+
Unchanged reads never refill a field, so application invalidations survive refresh and resubmission.
|
|
1605
|
+
The binding removes only its own custom-validity contribution, preserves authored custom errors,
|
|
1606
|
+
and withholds visual feedback until touch. Application changes to custom validity require an input
|
|
1607
|
+
event or explicit refresh because the platform emits no custom-validity mutation event.
|
|
1608
|
+
|
|
1609
|
+
### Represent a native control
|
|
1610
|
+
|
|
1611
|
+
The `write` event carries the native control after its projected value changes. Multiple-select
|
|
1612
|
+
options finish changing before the event. Unchanged values and metadata changes emit no `write`.
|
|
1613
|
+
Use the construction hook to observe initial projection, or subscribe through the binding's emitter
|
|
1614
|
+
for subsequent writes. A listener can refresh its presentation without dispatching input or change.
|
|
1615
|
+
|
|
1616
|
+
Map a native control to its presenting focus target through `presentations`. Leaving that target
|
|
1617
|
+
touches the native field; moving focus from the native control to its presentation does not. The
|
|
1618
|
+
mapping also directs summary links, invalid-submit focus, and feedback attributes to the presenter.
|
|
1619
|
+
It stops participating when the native control leaves the form. The presenting input's text
|
|
1620
|
+
does not become the native control's answer. A component still owns its selection, popup, focus
|
|
1621
|
+
forwarding, and native validation bubble behavior.
|
|
1622
|
+
|
|
1623
|
+
For a select presented by an input, pass the native select and the visible input when binding.
|
|
1624
|
+
|
|
1625
|
+
```ts
|
|
1626
|
+
import { createFormBinding } from '@orkestrel/form/browser'
|
|
1627
|
+
|
|
1628
|
+
const binding = createFormBinding(element, model, {
|
|
1629
|
+
presentations: new Map([[select, input]]),
|
|
1630
|
+
on: {
|
|
1631
|
+
write: (control) => {
|
|
1632
|
+
if (control === select) input.value = select.selectedOptions[0]?.textContent ?? ''
|
|
1633
|
+
},
|
|
1634
|
+
},
|
|
1635
|
+
})
|
|
1636
|
+
```
|
|
1637
|
+
|
|
1638
|
+
### Bind Mail Compose recipients over Veneer Input tags
|
|
1639
|
+
|
|
1640
|
+
Given a mounted Veneer `tags` instance, bind its native select and presenting input. The tags
|
|
1641
|
+
component commits free text with native input and change events. Initialize it before binding so
|
|
1642
|
+
the presenting input has no submitted name.
|
|
1643
|
+
|
|
1644
|
+
```ts
|
|
1645
|
+
import { createForm } from '@orkestrel/form'
|
|
1646
|
+
import { createFormBinding } from '@orkestrel/form/browser'
|
|
1647
|
+
|
|
1648
|
+
const select = tags.control
|
|
1649
|
+
const element = select.form
|
|
1650
|
+
if (element === null) throw new Error('Recipients require a form')
|
|
1651
|
+
const model = createForm({
|
|
1652
|
+
fields: [
|
|
1653
|
+
{
|
|
1654
|
+
control: 'list',
|
|
1655
|
+
name: select.name,
|
|
1656
|
+
label: 'To',
|
|
1657
|
+
default: Array.from(select.selectedOptions, (option) => option.value),
|
|
1658
|
+
rule: { required: true, maximum: 50, email: true },
|
|
1659
|
+
},
|
|
1660
|
+
],
|
|
1661
|
+
})
|
|
1662
|
+
const binding = createFormBinding(element, model, {
|
|
1663
|
+
presentations: new Map([[select, tags.input]]),
|
|
1664
|
+
on: {
|
|
1665
|
+
write: (control) => {
|
|
1666
|
+
if (control === select) tags.refresh()
|
|
1667
|
+
},
|
|
1668
|
+
render: () => tags.refresh(),
|
|
1669
|
+
},
|
|
1670
|
+
})
|
|
1671
|
+
```
|
|
1672
|
+
|
|
1673
|
+
Read repeated recipients with `new FormData(element).getAll(select.name)`. On page teardown, destroy
|
|
1674
|
+
the binding before the tags component and the document. This replaces the closed-checkbox contact
|
|
1675
|
+
workaround and the scalar comma-splitting validator for recipient arrays.
|
|
1676
|
+
|
|
1677
|
+
### Keep a footer press stable
|
|
1678
|
+
|
|
1679
|
+
Set `defer: true` to hold blur-triggered touch while a primary pointer press on an enabled button
|
|
1680
|
+
associated with this form is active. This includes externally associated buttons and presses on
|
|
1681
|
+
their descendants. Touch resumes in a task after pointer release or cancellation, leaving the click
|
|
1682
|
+
sequence free of blur feedback that moves the button. Keyboard blur remains immediate; input reads
|
|
1683
|
+
and submission validation remain immediate. Reset and destruction discard deferred touch.
|
|
1684
|
+
|
|
1685
|
+
Enable deferral where feedback can move an action under the pointer.
|
|
1686
|
+
|
|
1687
|
+
```ts
|
|
1688
|
+
import { createFormBinding } from '@orkestrel/form/browser'
|
|
1689
|
+
|
|
1690
|
+
const binding = createFormBinding(element, model, { defer: true })
|
|
1691
|
+
```
|
|
1692
|
+
|
|
1693
|
+
### Native capability boundaries
|
|
1694
|
+
|
|
1695
|
+
The browser admits text, search, email, URL, telephone, hidden, password, textarea, number, range,
|
|
1696
|
+
date, time, datetime-local, color, confirm, radio, single select, multiple select, checkbox groups,
|
|
1697
|
+
and file inputs. Blank strings become absence through the core's answer predicate; false, zero, and
|
|
1698
|
+
empty lists remain answers. A password field accepts both `password` and `text` inputs, so a reveal
|
|
1699
|
+
toggle retains discovery, validation, and summary links without mirroring the value.
|
|
1700
|
+
Numeric bad input remains a native error and never becomes NaN or a
|
|
1701
|
+
valid absent answer. Explicit refresh discards that unreadable native text and repaints the document's
|
|
1702
|
+
answer; ordinary input, touch, and submit keep the text available for correction. Native constraints
|
|
1703
|
+
still apply, including authored custom errors. Compatible
|
|
1704
|
+
required and numeric/temporal bounds are reflected when appropriate; presence-only required never
|
|
1705
|
+
becomes must-be-checked for confirm or must-be-nonempty for a list.
|
|
1706
|
+
|
|
1707
|
+
Native disabled controls, including disabled fieldsets with their first-legend exception, exclude
|
|
1708
|
+
a mounted field when every control in its group is disabled. A partially disabled choice group reads
|
|
1709
|
+
its enabled controls. Declared and observable application disables retain their logical authority.
|
|
1710
|
+
The binding reverses only its own native exclusion. Repeating disable while the field is already
|
|
1711
|
+
excluded is the core's no-op; it does not acquire a second exclusion lease. A removed population
|
|
1712
|
+
releases the binding's exclusion and retains its answer. Locked values remain logically enabled;
|
|
1713
|
+
text-like controls become readonly, while selections repaint the document's answer on mutation.
|
|
1714
|
+
|
|
1715
|
+
Files carry names in the document and bytes only in the native input. A mismatching document value
|
|
1716
|
+
clears the native selection; a nonempty saved filename never repopulates bytes. Explicit reading of
|
|
1717
|
+
an empty file picker imports an empty list. Display retained filenames separately when an application
|
|
1718
|
+
needs them, and keep upload transport outside the binding. Native range/color defaults and calendar
|
|
1719
|
+
sanitization also remain browser capabilities: a core value the browser cannot represent can display
|
|
1720
|
+
the native empty/default value, and a subsequent read imports that displayed answer.
|
|
1721
|
+
|
|
1722
|
+
Destroy disconnects observation and listeners, removes owned feedback and summary nodes, restores
|
|
1723
|
+
owned attributes and validity, and releases binding-owned native exclusion. It retains control values
|
|
1724
|
+
and authored content, and leaves the document alive. Reads and refreshes after destroy do nothing.
|
|
1725
|
+
|
|
1367
1726
|
## Events
|
|
1368
1727
|
|
|
1369
1728
|
Each event carries what a listener needs to act without reading the form back.
|
|
@@ -1375,6 +1734,10 @@ Each event carries what a listener needs to act without reading the form back.
|
|
|
1375
1734
|
| `disable` | the field's `name` | Once per field taken out of the form, in schema order. A call that moves nothing announces nothing. A `clear` is the exception; see its row. |
|
|
1376
1735
|
| `enable` | the field's `name` | Once per field put back into the form, in schema order. A call that moves nothing announces nothing. A `clear` is the exception; see its row. |
|
|
1377
1736
|
| `submit` | the submitted `FormValues` | On the submit that settles the form, and only that one. |
|
|
1737
|
+
| `touch` | the field's `name` | On the first touch since clear, including fields touched by preparation or failed terminal submission. |
|
|
1738
|
+
| `prepare` | the `FormSubmission` receipt | After installing the validated snapshot as pending. |
|
|
1739
|
+
| `accept` | the `FormSubmission` receipt | After adopting its captured enabled answers into baseline and consuming the pending identity. |
|
|
1740
|
+
| `abort` | the `FormSubmission` receipt | After consuming the pending identity and aborting its signal, including clear and destroy. |
|
|
1378
1741
|
| `clear` | nothing | On a completed `clear`, before any `validate` it caused. `clear` is the whole announcement of the reset: restored answers emit no `fill`, and the overlay reset emits no `disable` or `enable`. A custom-validator throw during reevaluation resets state but emits no `clear` and leaves the previous errors. |
|
|
1379
1742
|
| `abandon` | nothing | On the `destroy` that abandons an unsettled form. Never on a settled one. |
|
|
1380
1743
|
|
|
@@ -1569,17 +1932,30 @@ call signature and no named members.
|
|
|
1569
1932
|
|
|
1570
1933
|
#### `FormInterface`
|
|
1571
1934
|
|
|
1572
|
-
| Method | Returns | Summary
|
|
1573
|
-
| ------------ | -------------------------- |
|
|
1574
|
-
| `field` | `FormField` or `undefined` | Finds one field by name.
|
|
1575
|
-
| `fill` | `void` | Answers one field, or several at once.
|
|
1576
|
-
| `touch` | `void` | Records that somebody has visited a field.
|
|
1577
|
-
| `invalidate` | `
|
|
1578
|
-
| `disable` | `void` | Takes one field, several fields, or every field out of the form.
|
|
1579
|
-
| `enable` | `void` | Puts one field, several fields, or every field back into the form.
|
|
1580
|
-
| `submit` | `FormResult` | Checks every answer and settles the form when they all pass.
|
|
1581
|
-
| `
|
|
1582
|
-
| `
|
|
1935
|
+
| Method | Returns | Summary |
|
|
1936
|
+
| ------------ | -------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
1937
|
+
| `field` | `FormField` or `undefined` | Finds one field by name. |
|
|
1938
|
+
| `fill` | `void` | Answers one field, or several at once. |
|
|
1939
|
+
| `touch` | `void` | Records that somebody has visited a field. |
|
|
1940
|
+
| `invalidate` | `boolean` | Fails a field from outside, for what the rules cannot see. |
|
|
1941
|
+
| `disable` | `void` | Takes one field, several fields, or every field out of the form. |
|
|
1942
|
+
| `enable` | `void` | Puts one field, several fields, or every field back into the form. |
|
|
1943
|
+
| `submit` | `FormResult` | Checks every answer and settles the form when they all pass. |
|
|
1944
|
+
| `prepare` | `FormPreparationResult` | Prepares an owned enabled snapshot for acknowledgement while leaving the form editable. |
|
|
1945
|
+
| `accept` | `boolean` | Adopts the active submission's captured answers into the baseline without changing live state. |
|
|
1946
|
+
| `abort` | `boolean` | Consumes the active submission and aborts its signal without changing answers or baseline. |
|
|
1947
|
+
| `clear` | `void` | Returns every answer to its acknowledged baseline and aborts pending acknowledgement. |
|
|
1948
|
+
| `destroy` | `void` | Tears the form down, abandoning it when it has not settled. |
|
|
1949
|
+
|
|
1950
|
+
#### `FormBindingInterface`
|
|
1951
|
+
|
|
1952
|
+
The native binding exposes only synchronization and teardown methods.
|
|
1953
|
+
|
|
1954
|
+
| Method | Returns | Summary |
|
|
1955
|
+
| --------- | ------- | ------------------------------------------------------------------------- |
|
|
1956
|
+
| `read` | `void` | Imports mounted enabled controls without acknowledging their answers. |
|
|
1957
|
+
| `refresh` | `void` | Rediscovers controls and projects the document into the native form. |
|
|
1958
|
+
| `destroy` | `void` | Releases owned DOM effects and listeners without destroying the document. |
|
|
1583
1959
|
|
|
1584
1960
|
## Errors
|
|
1585
1961
|
|
|
@@ -1594,6 +1970,7 @@ throw is the caller's exception and escapes unchanged.
|
|
|
1594
1970
|
| `CONTROL` | A value written or seeded is one its field's control cannot hold. |
|
|
1595
1971
|
| `SETTLED` | A write reached a form that has already settled. |
|
|
1596
1972
|
| `ABANDONED` | A write reached a form that was destroyed before it settled, or `answer` rejected for that reason. |
|
|
1973
|
+
| `PENDING` | Preparation or terminal submission overlaps preparation or an active receipt, or clear interrupts preparation. |
|
|
1597
1974
|
|
|
1598
1975
|
```ts
|
|
1599
1976
|
import { createForm, isFormError } from '@orkestrel/form'
|
|
@@ -1658,7 +2035,7 @@ These invariants hold across [`src/core`](../src/core) and this guide.
|
|
|
1658
2035
|
`validate` listener wrote during that submit, `errors` can already differ from it.
|
|
1659
2036
|
5. **`valid` and `dirty` are derived.** Both are computed on read, from `errors` and from the
|
|
1660
2037
|
answers against `baseline`, so neither can drift from what the form holds. `baseline` itself is
|
|
1661
|
-
|
|
2038
|
+
advances only for enabled names captured by an accepted submission. Later edits remain dirty.
|
|
1662
2039
|
6. **A write is all-or-nothing.** `fill` checks every answer against its control before writing any,
|
|
1663
2040
|
and `disable` and `enable` check every name against the schema before any field moves, so a
|
|
1664
2041
|
`FIELD` or `CONTROL` failure leaves the form exactly as it was.
|
|
@@ -1671,7 +2048,8 @@ These invariants hold across [`src/core`](../src/core) and this guide.
|
|
|
1671
2048
|
listener is that one settlement: the outer call returns it, `submit` still fires once, and nothing
|
|
1672
2049
|
is evaluated after it.
|
|
1673
2050
|
8. **`undefined` is the only absence.** A field is unanswered when `values` holds no key for it, and
|
|
1674
|
-
answered otherwise
|
|
2051
|
+
answered otherwise. `required` accepts `''`, whitespace, `false`, and `0`; an empty `[]` passes
|
|
2052
|
+
for `checkbox` and `file`, while `list` requires an entry. This is
|
|
1675
2053
|
not HTML's model, which fails `required` on the exact empty string. A binding that wants HTML's
|
|
1676
2054
|
answer projects at the binding — `fill(name, matchesAnswer(raw) ? raw : undefined)` — and core
|
|
1677
2055
|
evaluation never applies that projection itself.
|
|
@@ -1738,12 +2116,12 @@ next change knows what it is reopening. `Layer` names who owns the concept, and
|
|
|
1738
2116
|
| Warnings and first-error-only modes | host | Every failure is an error and `errors` carries all of them in order. A severity axis, or a switch that reports only the first, is display policy applied over that list. |
|
|
1739
2117
|
| Server-error bags | host | A map of field names to server messages is a loop over `invalidate`, which already holds one external failure per field and drops it when the field is refilled. |
|
|
1740
2118
|
| Form-level validators | seam | A rule about the form as a whole. `custom` already reads every answer, so the same check runs today attached to the field it would fail. |
|
|
1741
|
-
| Address lists | seam | `
|
|
2119
|
+
| Address lists | seam | `ListField` holds free-text arrays; its `email` rule checks every address and names failures. |
|
|
1742
2120
|
| Group-level disable | seam | `disable` takes no group argument. A host expands a group from the schema in one line, and a second way to name the same set is a second thing to keep consistent. |
|
|
1743
2121
|
| Computed fields | host | A field deriving its value from siblings would be a second writer of `values` and could disagree with `fill`. The host computes and fills, so one writer stays one writer. |
|
|
1744
2122
|
| Async and live choices | host | `choices` is data in the schema. A list fetched or filtered while somebody types is the host building a new schema, which `parseForm` audits for it. |
|
|
1745
2123
|
| Drafts and autosave | host | `values` is readable at any moment and `parseValues` reads a stored record back, so persistence is a host loop over `values` and `parseValues` with the host's own storage and cadence. |
|
|
1746
|
-
| Undo and history | host | The form holds
|
|
2124
|
+
| Undo and history | host | The form holds live answers and `baseline` holds acknowledged answers. A history stack and its retention policy belong to the host. |
|
|
1747
2125
|
| Schema migration | host | Versioning a stored schema and moving old answers onto a new one is the host's data problem. `parseForm` and `parseValues` are the gates on each side of it. |
|
|
1748
2126
|
| Trim and normalize | binding | The form stores what it is given. Trimming, case folding, and Unicode normalization belong to the binding, and `matchesAnswer` is where a value that is blank after trimming becomes absence. |
|
|
1749
2127
|
| Localization | host | `FormOptions.messages` replaces a rule's copy, and `label` and `help` are the schema author's strings. Locale selection, plurals, and message catalogs are the host's. |
|
|
@@ -1751,11 +2129,19 @@ next change knows what it is reopening. `Layer` names who owns the concept, and
|
|
|
1751
2129
|
| Group and choice `meta` | out | `meta` is on `FieldBase` alone, because a field carrier is what the first consumer asked for. The exact guards refuse it on `FormGroup` and `FieldChoice` until one asks. |
|
|
1752
2130
|
| Constraint-API binding | `src/browser` | Reflecting `required` and `minimum` onto real elements, and reading `validity` back, is binding work. It belongs where the elements are. |
|
|
1753
2131
|
| `FormData` and duplicate names | `src/browser` | Several values under one name, the submitter button, and the `FormData` encoding are host wire formats. `FormValues` holds one answer per field name by design. |
|
|
1754
|
-
| Browser binding | `src/browser` |
|
|
2132
|
+
| Browser binding | `src/browser` | The native binding projects an existing document, admits native submission and reset, and owns neutral validation feedback. Applications retain persistence and navigation policy. |
|
|
1755
2133
|
| Terminal adoption | terminal | A terminal driver parking a whole form rather than one prompt belongs in the terminal package, on the same `answer` promise this document already exposes. |
|
|
1756
2134
|
|
|
1757
2135
|
## Tests
|
|
1758
2136
|
|
|
2137
|
+
- [`tests/src/browser/FormBinding.test.ts`](../tests/src/browser/FormBinding.test.ts) covers native
|
|
2138
|
+
ownership, events, disabled state, feedback, focus, pending refusal, and reentrant teardown.
|
|
2139
|
+
- [`tests/src/browser/helpers.test.ts`](../tests/src/browser/helpers.test.ts) covers the native control
|
|
2140
|
+
population and file-name boundary; [`tests/src/browser/validators.test.ts`](../tests/src/browser/validators.test.ts)
|
|
2141
|
+
covers total native guards.
|
|
2142
|
+
- [`tests/src/browser/integration.test.ts`](../tests/src/browser/integration.test.ts) executes the
|
|
2143
|
+
native guide example and a real keyboard/click editing and acknowledgement flow.
|
|
2144
|
+
|
|
1759
2145
|
- [`tests/guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔ barrel bijection, the
|
|
1760
2146
|
`FormInterface` ↔ `Form` method bijection, and the equality gate: every `Summary` cell against its
|
|
1761
2147
|
declaration's description paragraph, the titled `Open a form, answer it, and settle it` fence
|