@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.
Files changed (88) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +84 -1
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +21 -6
  3. package/dist/agents/skills/orkestrel-harden/scripts/titles.js +338 -0
  4. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +3 -39
  5. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +433 -47
  6. package/dist/agents/skills/orkestrel-publish/scripts/window.js +2 -1
  7. package/dist/bin/main.js +16 -9
  8. package/dist/bin/main.js.map +1 -1
  9. package/dist/host/agents/orchestration.md +3 -3
  10. package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +2 -2
  11. package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
  12. package/dist/host/agents/skills/orkestrel-build/SKILL.md +2 -2
  13. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +2 -2
  14. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +1 -1
  15. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
  16. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
  17. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +1 -0
  18. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +100 -1
  19. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +27 -6
  20. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +4 -4
  21. package/dist/host/agents/skills/orkestrel-falsify/agents/openai.yaml +1 -1
  22. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +8 -7
  23. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +6 -6
  24. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +6 -1
  25. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +1700 -0
  26. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -2
  27. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +48 -0
  28. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +4 -2
  29. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +425 -0
  30. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +2 -2
  31. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +9 -8
  32. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +3 -3
  33. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +2 -2
  34. package/dist/host/agents/skills/orkestrel-publish/references/release.md +1 -0
  35. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +56 -13
  36. package/dist/host/agents/skills/orkestrel-publish/references/window.md +6 -6
  37. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +5 -49
  38. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +646 -49
  39. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +2 -1
  40. package/dist/host/agents/transports/codex.md +1 -1
  41. package/dist/host/claude/agents/orkestrel.md +54 -53
  42. package/dist/host/claude/agents/reviewer.md +4 -4
  43. package/dist/host/claude/rules/documentation.md +2 -1
  44. package/dist/host/claude/rules/patterns.md +1 -1
  45. package/dist/host/claude/rules/quality.md +2 -2
  46. package/dist/host/claude/rules/styles.md +1 -1
  47. package/dist/host/claude/rules/tests.md +34 -8
  48. package/dist/host/claude/rules/typescript.md +29 -0
  49. package/dist/host/claude/rules/workspace.md +32 -22
  50. package/dist/host/claude/rules/writing.md +34 -26
  51. package/dist/host/claude/settings.json +2 -0
  52. package/dist/host/claude/skills/orkestrel-debrief/SKILL.md +1 -1
  53. package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +1 -1
  54. package/dist/host/codex/agents/analyst.toml +3 -3
  55. package/dist/host/codex/agents/reviewer.toml +4 -4
  56. package/dist/host/configs/policy.ts +13 -2
  57. package/dist/host/dotfiles/editorconfig +0 -1
  58. package/dist/host/dotfiles/gitattributes +0 -1
  59. package/dist/host/dotfiles/gitignore +0 -1
  60. package/dist/host/dotfiles/oxlintrc.json +24 -0
  61. package/dist/host/guides/agent.md +1638 -598
  62. package/dist/host/guides/budget.md +89 -29
  63. package/dist/host/guides/form.md +477 -91
  64. package/dist/host/guides/mcp.md +14 -2
  65. package/dist/host/guides/ollama.md +97 -73
  66. package/dist/host/guides/probe.md +1 -1
  67. package/dist/host/guides/scaffold.md +158 -101
  68. package/dist/host/guides/study.md +2635 -0
  69. package/dist/host/guides/table.md +225 -15
  70. package/dist/host/guides/terminal.md +22 -20
  71. package/dist/host/guides/test.md +689 -255
  72. package/dist/host/manifest.json +87 -63
  73. package/dist/host/tests/config.test.ts +136 -82
  74. package/dist/host/tests/policy.test.ts +39 -1
  75. package/dist/host/tests/setupPolicy.ts +86 -28
  76. package/dist/src/core/index.cjs +1387 -611
  77. package/dist/src/core/index.cjs.map +1 -1
  78. package/dist/src/core/index.d.cts +35 -20
  79. package/dist/src/core/index.d.ts +35 -20
  80. package/dist/src/core/index.js +1385 -612
  81. package/dist/src/core/index.js.map +1 -1
  82. package/dist/src/server/index.cjs +12 -12
  83. package/dist/src/server/index.cjs.map +1 -1
  84. package/dist/src/server/index.d.cts +12 -12
  85. package/dist/src/server/index.d.ts +12 -12
  86. package/dist/src/server/index.js +12 -12
  87. package/dist/src/server/index.js.map +1 -1
  88. package/package.json +4 -4
@@ -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 one submit that settles the form exactly once.
5
+ > satisfy, acknowledged editing, and a terminal submit that settles the form exactly once.
6
6
 
7
- **A terminal prompt and a browser form are the same abstraction.** Both ask a person a set of
8
- questions, hold partial answers, check them against rules, and finish once. What differs is the
9
- host, and each host contributes the one part it owns. Parking is the server environment's
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 is the browser's contribution, and it lives in the browser, not here. Nothing here
13
- renders, reads a keyboard, or opens a socket. This package ships the document both hosts share.
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
- Everything in this guide is exported from `@orkestrel/form` ([`src/core`](../src/core)). Nothing is
23
- internal: every declaration in the module is reachable from the barrel, so a consumer holds exactly
24
- the mechanisms the package uses on itself.
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 | 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 \| 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' \| '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
- | `FileField` | interface | `FieldBase plus { control, accept?, multiple? }` | Represents one or more files, by name. |
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 | Kind | Shape | Summary |
107
- | --------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
108
- | `Form` | class | `FormInterface` | Implements `FormInterface` exactly, over an owned schema, the answers given against it, and the errors they carry. |
109
- | `FormInterface` | interface | `{ emitter, schema, values, baseline, errors, touched, disabled, status, valid, dirty, answer } plus field, fill, touch, invalidate, disable, enable, submit, clear, destroy` | Declares the contract a form exposes: the state it holds and the calls that move it. |
110
- | `createForm` | function | `(schema: FormSchema, options?: FormOptions) => FormInterface` | Opens a form against a schema. |
111
- | `FormOptions` | interface | `{ on?, error?, values?, messages? }` | Describes how to open a form. |
112
- | `FormStatus` | type | `'editing' \| 'settled' \| 'abandoned'` | Represents where a form sits in its life. |
113
- | `FormResult` | type | `Result<FormValues, readonly FieldError[]>` | Reports what a submit answers with: the values, or every error that stopped them. |
114
- | `FormEventMap` | type | `{ fill, validate, disable, enable, submit, clear, abandon }` | Lists everything a form announces. |
115
- | `FormError` | class | `new (code: FormErrorCode, message: string, context?: JSONRecord) => FormError` | Represents an error raised by the form domain. |
116
- | `FormErrorCode` | type | `'SCHEMA' \| 'FIELD' \| 'CONTROL' \| 'SETTLED' \| 'ABANDONED'` | Names the machine-readable code a form error carries. |
117
- | `isFormError` | function | `FormError` | Determines whether an unknown value is a form error. |
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, which every scaffold target carries at
546
- `.agents/skills/enterprise-bootstrap/references/inputs.md`. The category names in the
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, whose value keeps the order it
580
- was filled in. Tags a person types freely are outside the document: `checkbox` admits a listed
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` | That an answer exists at all. Presence only: `''`, `[]`, `false`, and `0` are answers and satisfy it. |
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 only. |
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, so `required`
639
- is satisfied by every one of them. `required` asks whether an answer exists, never whether it says
640
- anything: a length rule is `minimum`, and a "must be ticked" rule is `custom`.
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
- The same seam closes a list of addresses. `TextField` has no `multiple`, because that word already
721
- means a list of file names on `FileField` and one word cannot hold two value shapes. A field that
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`. Every getter keeps answering afterwards.
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
- once the answers differ from `baseline`, the ones the form opened with. Neither is stored, so
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` is those opening answers, held as a value: the schema's defaults overlaid with any seeded
1185
- `values`, fixed when the form opens and never moved again. It is what `dirty` measures against and
1186
- what `clear` returns to, so a host that wants to know _which_ answers moved — not merely that one
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`, so the form reads exactly as it opened.
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 it opened with, fixed for its whole life
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 a submit
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` | `void` | Fails a field from outside, for what the rules cannot see. |
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
- | `clear` | `void` | Returns every answer to the ones the form opened with. |
1582
- | `destroy` | `void` | Tears the form down, abandoning it when it has not settled. |
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
- fixed when the form opens and never moves again.
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, so `''`, whitespace, `[]`, `false`, and `0` all satisfy `required`. This is
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 | `TextField` has no `multiple`: that word already means a list of file names on `FileField`, and one word cannot carry two value shapes. Several addresses is `custom` plus the exported `EMAIL_PATTERN`. |
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 the answers and `baseline` holds the ones it opened with. A stack of everything in between is a host concern with a host's retention policy. |
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` | Binding a schema to real elements belongs in a future `src/browser`, taking a form and an element. It is not in this round because nothing renders here yet. |
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