let d = import "adr-defaults.ncl" in d.make_adr { id = "adr-017", title = "TypeDialog — Schema-Driven Web Form Backend for Workspace Configuration", status = 'Accepted, date = "2026-01-08", context = "Workspace configuration requires validated user input across multiple fields with interdependencies (e.g. selecting 'kubernetes' deployment enables K8s-specific options). Nushell's `input` command is single-line text only — no validation, no conditional fields, no multi-user collaboration. Nickel is declarative and cannot prompt users interactively. Two options considered: (1) TUI dialogs — Rust-native terminal forms, keyboard-driven, works over SSH; (2) Web form backend — browser-accessible, schema-generated forms, multi-user workflow support.", decision = "TypeDialog implements a schema-driven web form backend. Nickel contracts in `.typedialog/provisioning/schemas/` are the single source of truth; form fields are generated directly from schema types and constraints with zero manual form code. The web UI is embedded in the control center dashboard (accessible from any browser) and supports draft → review → approve workflows for team environments. A TUI fallback is available for SSH-only environments. Generated config is written as NCL and validated against the same contracts that drove form generation.", rationale = [ { claim = "Schema drift between form and config is structurally impossible", detail = "Form fields are generated from Nickel contracts at render time. If a schema changes, the form changes. No manual sync required — the source of truth is a single file.", }, { claim = "Web UI enables multi-user collaborative workflows impossible with TUI", detail = "Platform engineers, security teams, and dev leads can each access the form from their own browser without SSH. Draft configs persist across sessions. Approval flows are built into the form lifecycle.", }, { claim = "TypeDialog fragments encode valid configuration combinations", detail = "Fragments (database-postgres, deployment-k8s, etc.) capture the full parameter space for each configuration pattern. The fragment system prevents invalid combinations (e.g. PostgreSQL-specific options appearing for a SQLite config) without runtime validation logic.", }, ], consequences = { positive = [ "Zero manual form code — schema changes propagate to UI automatically", "Multi-user collaboration via browser; no SSH required", "Draft config persistence with audit trail of who configured what", "TypeDialog fragments prevent invalid config combinations structurally", ], negative = [ "Control center must be running for web UI access — SSH-only environments use TUI fallback", "Form generation requires contract files to be valid NCL — schema errors surface as broken forms, not compile errors", ], }, alternatives_considered = [ { option = "TUI-only (Ratatui forms, keyboard-driven)", why_rejected = "Single-user, requires interactive terminal, no multi-user collaboration, no draft persistence, no browser access. Adequate for solo mode but insufficient for team deployments.", }, { option = "Custom HTML forms maintained separately from schemas", why_rejected = "Manual maintenance creates drift between form fields and schema types. Every schema change requires a form update. Two sources of truth.", }, ], constraints = [ { id = "schema-is-form-source-of-truth", claim = "TypeDialog form fields must be generated from Nickel contracts — no manual form field definitions allowed", scope = ".typedialog/provisioning/schemas/", severity = 'Hard, check = { tag = 'FileExists, path = ".typedialog/provisioning/schemas/provisioning-config.ncl", present = true }, rationale = "Manual form definitions diverge from schemas. The contract is the form.", }, { id = "generated-config-validates-against-contract", claim = "Config output by TypeDialog must validate against the same schema contracts used for form generation", scope = ".typedialog/provisioning/generated/", severity = 'Hard, check = { tag = 'FileExists, path = ".typedialog/provisioning/generated", present = true }, rationale = "A form that produces invalid config provides false confidence. Validation must be end-to-end.", }, ], related_adrs = ["adr-013-surrealdb-global-store", "adr-014-solid-enforcement"], ontology_check = { decision_string = "TypeDialog generates web forms from Nickel contracts; schema contracts are the single source of truth for both validation and UI generation", invariants_at_risk = ["type-safety-nickel"], verdict = 'Safe, }, }