Milestone 6 (final) of the Configuration Engine sprint. - docs/EDITOR.md: new "Configuration schema, form engine, and validation architecture" section covering the field-schema registry, centralized validators, live inline feedback, undo/redo, modified-field tracking, and pre-publish preview added in M1-M5. Updated the facade signal list and folder tour to include schema/. - ADR-0002 (docs/context/adrs/): records the metadata-augmented-vs-fully- schema-driven decision, why severity splits blocking/advisory, and the accepted debt (partial [error] binding coverage, schema not yet driving template labels). - FACTS.jsonl (project-editor): decision fact pointing at the ADR. Note: `barry-cache` is a phantom devDependency (no bin resolves, confirmed in M1) - ADR/FACTS were authored by hand matching the existing schema/format rather than via `npm run barry -- adr new` / `validate`. Gate: tsc --noEmit, npm test (33/33), arch:check, build all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.7 KiB
id, title, status, date, tags
| id | title | status | date | tags | ||||
|---|---|---|---|---|---|---|---|---|
| ADR-0002 | Project Editor field-schema registry, centralized validation, and metadata-augmented form engine | active | 2026-07-16 |
|
Context
The Project Editor (src/app/features/project-editor/) edits the tenant BootstrapConfig across 11 hand-authored section templates, all built on the shared/ui field primitives (ADR established post-Sprint 30 redesign; see docs/EDITOR.md). Field labels/hints/defaults lived inline per template, ProjectValidator issues were not addressable to a field, there was no undo/redo, and no per-field modified/error state. Sprint X+1 ("Configuration Engine & Dynamic Form Foundation") required: a field-schema registry, centralized validation (JSON/CSS/URL/color/locale/duplicate-route/widget-config), live inline validation with publish-gating, pre-publish preview, dirty/modified-field tracking with a leave-warning, and session undo/redo — without duplicating form logic or validators, and without breaking draft/publish/import/export.
Decision
Metadata-augmented, not fully schema-driven. A field-schema registry (schema/field-schema.model.ts, schema/editor-schema.ts, schema/editor-schema.service.ts) declares every editable field (dot-path key, section, type, label/hint keys, default, required, validator refs) as the single source of truth for field identity and validator wiring — but section templates stay hand-authored. The schema drives validation and metadata; it does not render fields. This was chosen over a fully schema-driven renderer because 11 mature templates already exist on top of the shared/ui kit, and a renderer rewrite carried materially higher regression risk against "preserve all existing functionality" for no UX gain.
Validators are pure, composed, and tagged. schema/validators/primitives.ts holds one pure function per concern (hex color, HTTP URL, email, JSON, CSS brace-balance, style-block extraction, route normalization). ProjectValidator composes them and attaches section, fieldKey, and severity (error | warning) to every issue, so the same validator is never re-implemented per field or per section.
Severity splits blocking from advisory. publish() now gates on hasBlockingIssues() (severity === 'error') instead of "any issue exists." All 9 pre-existing checks stayed error (no behavior change); the new duplicate-routes and invalid-CSS checks are warning — informative, non-blocking, by design.
Undo/redo is a pure reducer wrapped in debounced facade state. schema/history.util.ts is a framework-free {past, future} snapshot reducer (commit/undo/redo, depth-capped). The facade debounces commits (~300ms) so a typing burst collapses into one undo step, and routes undo/redo through the same localStorage draft-save path as every other mutation so the autosave never desyncs from the undo stack.
Modified-field tracking is a schema diff, not a form-state library. modifiedFields walks every schema field and compares current vs. originalBootstrap by dot-path — no new dependency, reuses EditorSchemaService.getByPath.
Consequences
Positive:
- One registry answers "what fields exist, what validates them, what do they mean" — new fields register once and get validation + inline-error wiring for free.
- No validator is duplicated: JSON/CSS/color/URL/email logic lives in exactly one place each.
- Zero changes to
ProjectEditorIoService,ProjectEditorDraftStorageService, or the draft/publish/reset flow — full backward compatibility. - Undo/redo and modified-field tracking added without a state-management library.
Negative / accepted debt:
- Inline
[error]binding is wired on a subset of fields (theme palette, general name/domain, branding logo) — not yet every schema-backed field across all 11 sections. Section-level visibility (nav badges, save-bar issue list) covers the rest today. - The field-schema registry is not yet consumed by templates for label/hint rendering (still inline i18n keys in each template) — only for validation, diffing, and change-summary labels. A future pass could fully drive labels from the schema.
- CSS/JSON validators have a thin binding surface today (CSS only via static-page
<style>blocks; JSON only via import) since no dedicatedcustomCss/raw-JSON field exists yet inBootstrapConfig.
Compliance Requirements
- New editable
BootstrapConfigfields should get aFieldSchemaentry ineditor-schema.tsalongside their template addition. - New validation rules must be added as a pure function in
schema/validators/primitives.tsand composed intoProjectValidator— never inlined ad hoc in a section component. severity: 'error'is reserved for checks that must block Publish; anything advisory is'warning'.