Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
# Design: enterprise-form-builder
> Non-normative. Architecture, file layout, and integration narrative for
> `@suaveplan/form-builder`. The normative contract is `./specs/form-builder/spec.md`.
## Tier & dependencies
Universal-tier (runs in Bun, Node, browser without polyfills). Direct deps:
- `@suaveplan/error` (`workspace:^`) — `SuaveplanError`, `defineCodes`.
- `@suaveplan/events` (`workspace:^`) — `EventBus<FormEventMap>` for state-change emission.
- `@suaveplan/types` (`workspace:^`) — `Clock`, `IdGenerator` for injectable time / id.
- `@opentelemetry/api` (`catalog:observability`) — counters + histograms.
- `tslib` (`catalog:build`).
- `zod` (`catalog:forms`) — boundary validation + Zod schema generator.
`devDependencies`: `@suaveplan/testing`, `@vitest/coverage-istanbul`, `vite`, `vite-plugin-dts`,
`vitest`, `typescript`, `@types/node`.
## File layout
```text
packages/ui/forms/form-builder/
package.json
tsconfig.lib.json
vite.config.ts
vitest.config.ts
README.md
tests/setup/polyfills.ts
src/
index.ts # root barrel (named re-exports only)
types/form.types.ts # shared structural types (no .md)
constants/index.ts # FIELD_TYPES, OPERATORS, defaults
schema/schema/
schema.ts # Zod schemas + parse/validate
schema.test.ts
schema.md
index.ts
fields/field-registry/
field-registry.ts # FieldRegistry + 17 built-ins
field-registry.test.ts
field-registry.md
index.ts
validation/validation/
validation.ts # runRule, runField, runForm (sync + async)
validation.test.ts
validation.md
index.ts
validation/validators/
validators.ts # 11 built-in rule runners
validators.test.ts
validators.md
index.ts
conditional/conditional/
conditional.ts # operator + boolean evaluator
conditional.test.ts
conditional.md
index.ts
conditional/dependency-graph/
dependency-graph.ts # topo sort + cycle detect
dependency-graph.test.ts
dependency-graph.md
index.ts
state/form-state/
form-state.ts # FormState machine
form-state.test.ts
form-state.md
index.ts
state/undo-redo/
undo-redo.ts # bounded history stack
undo-redo.test.ts
undo-redo.md
index.ts
builder/form-builder/
form-builder.ts # FormBuilder class (entry-point)
form-builder.test.ts
form-builder.md
index.ts
submission/submission/
submission.ts # submit pipeline
submission.test.ts
submission.md
index.ts
serialise/serialise/
serialise.ts # JSON round-trip
serialise.test.ts
serialise.md
index.ts
zod-schema/zod-schema/
zod-schema.ts # FormSchema → ZodObject
zod-schema.test.ts
zod-schema.md
index.ts
errors/form-error/
form-error.ts # defineCodes + FormError
form-error.test.ts
form-error.md
index.ts
otel/instruments/
instruments.ts # lazy-init counters + histograms
instruments.test.ts
instruments.md
index.ts
```
## Error codes — `FORM_BUILDER.*`
| Code | When |
|---|---|
| `FORM_BUILDER.FIELD_NOT_FOUND` | `getField`, `removeField`, `updateField` on missing id |
| `FORM_BUILDER.SECTION_NOT_FOUND` | `getSectionById`, `removeSection` on missing id |
| `FORM_BUILDER.DUPLICATE_ID` | `addField` / `addSection` with already-registered id |
| `FORM_BUILDER.CONFIG_INVALID` | Zod parse failure on `FormSchema` or empty `id`/`name` |
| `FORM_BUILDER.VALIDATION_FAILED` | `submit` aborted because validation produced errors |
| `FORM_BUILDER.CYCLIC_DEPENDENCY` | `DependencyGraph.topoSort` detects a cycle |
## Conditional logic
`ConditionalRule` is a discriminated union:
```ts
type ConditionalRule =
| { kind: "leaf"; field: string; operator: LeafOperator; value: unknown }
| { kind: "and"; rules: readonly ConditionalRule[] }
| { kind: "or"; rules: readonly ConditionalRule[] }
| { kind: "not"; rule: ConditionalRule };
```
Leaf operators: `eq`, `neq`, `in`, `notIn`, `gt`, `lt`, `gte`, `lte`, `contains`.
A `FieldDefinition.conditional` may be a single rule (backwards-compatible with the upstream
`{ field, operator, value }` shape; the deserialiser auto-wraps in a `leaf`).
## Dependency graph
`DependencyGraph` extracts every referenced field id from a `ConditionalRule` (and from
`equalsField` validators), forms a DAG keyed by field id, runs Kahn's topo sort, and emits
`CYCLIC_DEPENDENCY` if a cycle is found. The builder evaluates conditional visibility in topo
order so chains (`a → b → c`) settle in one pass.
## Undo / redo
Schema-mutation operations (`addField`, `removeField`, `updateField`, `addSection`,
`removeSection`) push a reverse-op onto a bounded `UndoRedoStack` (default capacity 100). `undo()`
applies the inverse and pushes the forward-op onto the redo stack; any new mutation clears the redo
stack. The stack is opt-in: `FormBuilder` exposes `enableHistory(capacity?)`.
## Zod schema generator
`toZodSchema(form)` walks `form.fields` and emits a `z.ZodObject` whose shape mirrors the field
types and validation rules. Conditional `required` is folded into the schema via
`z.unknown().superRefine` so consumers using `react-hook-form` or `@hookform/resolvers/zod` get
identical behaviour to the schema engine's `validateForm`.
## OTel instruments
Lazily initialised via `metrics.getMeter("@suaveplan/form-builder")`:
| Instrument | Kind | Attributes |
|---|---|---|
| `form_builder.field.mutated_total` | counter | `op: add\|remove\|update` |
| `form_builder.validation.run_total` | counter | `scope: field\|form`, `result: ok\|errors` |
| `form_builder.submission.total` | counter | `result: ok\|invalid\|failed` |
| `form_builder.validation.duration_ms` | histogram | `scope: field\|form` |
| `form_builder.submission.duration_ms` | histogram | `result: ok\|invalid\|failed` |
## Determinism
No `Date.now()` or `Math.random()` at module scope. The builder takes an optional `Clock` and
`IdGenerator` in its constructor; defaults derive from `@suaveplan/types`. Timestamps on
`FormSchema.createdAt` / `updatedAt` come from the injected clock.