Initial Commit
This commit is contained in:
171
openspec/archive/2026-05-25-enterprise-form-builder/design.md
Normal file
171
openspec/archive/2026-05-25-enterprise-form-builder/design.md
Normal 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.
|
||||
Reference in New Issue
Block a user