# 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` 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.