diff --git a/docs/README.md b/docs/README.md index bae1dfc..e3c01fc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,6 +38,14 @@ Complete documentation for using, building, and deploying TypeDialog. - Installation verification - CI/CD integration +## Architecture + +- **[architecture.md](architecture.md)** - Technical positioning and internal architecture + - Positioning vs Inquire, Ratatui, Axum, Nushell + - BackendFactory mechanics and feature-gated dispatch + - Three-phase form execution (selector fields, lazy fragments, field-by-field) + - Known technical debt + ## Configuration 1. **[configuration.md](configuration.md)** - Configuration guide @@ -70,6 +78,7 @@ Complete documentation for using, building, and deploying TypeDialog. ### Feature Guides +- [Architecture](architecture.md) - Positioning, BackendFactory, execution model - [Nickel Integration](nickel.md) - Schema parsing, contracts, i18n, templates - [Encryption](encryption/) - Secure field handling - [Provisioning Generator](prov-gen/) - Infrastructure as Code generation @@ -188,6 +197,7 @@ docs/ │ └── release.md ← Release workflow │ ├── Reference +│ ├── architecture.md ← Technical positioning & internals │ ├── field_types.md ← Field types reference │ └── nickel.md ← Nickel schema support │ diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..22020fb --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,226 @@ +# Architecture + +Technical positioning and internal architecture of TypeDialog. + +## Positioning + +TypeDialog is not a terminal prompt library — it is a **form orchestration layer** that sits two abstraction levels above tools like Inquire, Ratatui, or Axum. + +The comparison point is relevant: Inquire (`inquire = "0.9"`) is one of TypeDialog's dependencies, consumed as the render engine for the CLI backend. A user of TypeDialog never calls `Text::new("Name?").prompt()` directly — that call happens inside `InquireBackend::execute_field_sync`, invisible to the form author. + +### Abstraction Stack + +```text +┌─────────────────────────────────────────────────────┐ +│ typedialog │ +│ Declarative TOML forms + i18n + templates + AI │ +│ BackendFactory → dispatch by feature │ +├──────────────┬──────────────┬──────────────────────-┤ +│ CLI backend │ TUI backend │ Web backend │ +│ (Inquire) │ (Ratatui) │ (Axum) │ +└──────────────┴──────────────┴───────────────────────┘ + │ + Nushell plugin (nu-protocol) +``` + +### What TypeDialog Is Not + +| What the primitive does | TypeDialog | +|---|---| +| Library for use in your CLI app | Is the app — or the library others embed | +| Terminal input tool | Multi-surface entry point (CLI, TUI, Web, AI) | +| Comparable to Rhai or Nushell | Integrates Nushell via `nu-plugin`/`nu-protocol` as an output channel | + +### Differentiators + +**Unified schema** — a single TOML file describes the same form for CLI, TUI, and HTTP. No per-backend code. + +**Nushell as integration runtime** — outputs flow as structured data into Nu pipelines via the Nushell plugin protocol (`nu-plugin = "0.110.0"`), not as text. + +**AI backend** (`typedialog-ai` + `typedialog-agent`) — forms can be generated or processed by agents backed by SurrealDB (`kv-mem`), Tantivy full-text search, and HNSW vector search (`instant-distance`). + +**Native i18n** — Fluent bundles with system locale detection, no external tooling required. + +--- + +## BackendFactory + +### The `FormBackend` Trait + +All backends implement a single async trait (`backends/mod.rs:27`): + +```rust +#[async_trait] +pub trait FormBackend: Send + Sync { + async fn initialize(&mut self) -> Result<()>; + async fn render_display_item(&self, item: &DisplayItem, context: &RenderContext) -> Result<()>; + async fn execute_field(&self, field: &FieldDefinition, context: &RenderContext) -> Result; + + // Default impl: falls back to field-by-field via execute_field + // TUI and Web override this for complete-form rendering + async fn execute_form_complete(...) -> Result>; + + async fn shutdown(&mut self) -> Result<()>; + fn as_any(&self) -> &dyn std::any::Any; + fn is_available() -> bool where Self: Sized; + fn name(&self) -> &str; +} +``` + +`execute_form_complete` has a default implementation that delegates to `execute_field` sequentially. Backends that need full-form rendering (TUI, Web) override it. + +### Factory Dispatch + +`BackendFactory::create` returns `Box`. The selection is a compile-time `#[cfg(feature)]` gate combined with runtime match (`backends/mod.rs:109`): + +```rust +pub fn create(backend_type: BackendType) -> Result> { + match backend_type { + BackendType::Cli => { + #[cfg(feature = "cli")] + { Ok(Box::new(cli::InquireBackend::new())) } + #[cfg(not(feature = "cli"))] + { Err(/* clear message */) } + } + #[cfg(feature = "tui")] + BackendType::Tui => Ok(Box::new(tui::RatatuiBackend::new())), + #[cfg(feature = "web")] + BackendType::Web { port } => Ok(Box::new(web::WebBackend::new(port))), + } +} +``` + +The backend code for a disabled feature is not compiled into the binary. The trait object provides runtime polymorphism; the feature gate provides compile-time dead-code elimination. + +### Auto-Detection + +`BackendFactory::auto_detect` follows a fallback chain (`backends/mod.rs:132`): + +```text +TYPEDIALOG_BACKEND=tui → TUI (if feature active) +TYPEDIALOG_BACKEND=web → Web (port from TYPEDIALOG_PORT, default 9000) +default → Cli +``` + +### RenderContext + +Passed to every render and field execution call: + +```rust +pub struct RenderContext { + pub results: HashMap, // accumulated field values + pub locale: Option, // locale override +} +``` + +`results` carries the accumulated answers from previous fields. `InquireBackend::execute_field_sync` +receives this as `previous_results` and uses it for `options_from` filtering in `Select` fields. + +--- + +## Form Execution + +### Three-Phase Execution + +`execute_with_backend_two_phase_with_defaults` (`form_parser/executor.rs`) is the primary execution path: + +**Phase 1 — Selector fields first** + +Fields that control `when:` conditionals are identified and executed before loading any fragments. +This ensures conditional branches are known before lazy-loading dependent content. + +```rust +let selector_field_names = identify_selector_fields(&form); +// execute only selectors → populate results +``` + +**Phase 2 — Build element list with lazy loading** + +With Phase 1 results known, fragments (`includes:`) are loaded only if their controlling condition is met. + +```rust +let element_list = build_element_list(&form, base_dir, &results)?; +``` + +**Phase 3 — Execute remaining fields** + +Iterates the element list, evaluates `when:` conditions per-element, and dispatches to the backend. +Two sub-modes: + +- `DisplayMode::Complete` — passes all items and fields to `execute_form_complete` at once + (used by TUI and Web for reactive rendering) +- Field-by-field — sequential execution with condition evaluation per step (used by CLI) + +### Full Execution Flow + +```text +typedialog binary +│ +├── parse TOML → FormDefinition +├── BackendFactory::create(BackendType::Cli) → Box +├── execute_with_backend_two_phase_with_defaults(form, backend, i18n, base_dir, defaults) +│ │ +│ ├── Phase 1: selector fields → backend.execute_field() +│ ├── Phase 2: build_element_list() with lazy fragment loading +│ └── Phase 3: iterate → render_display_item() / execute_field() +│ +└── HashMap → JSON / YAML / TOML output +``` + +### CLI Field Execution + +The `InquireBackend` implements field execution as a synchronous function (`backends/cli.rs`): + +```rust +pub(crate) fn execute_field_sync( + &self, + field: &FieldDefinition, + previous_results: &HashMap, +) -> Result +``` + +The `FormBackend::execute_field` trait impl wraps it: + +```rust +async fn execute_field(&self, field: &FieldDefinition, context: &RenderContext) -> Result { + self.execute_field_sync(field, &context.results) +} +``` + +`filter_options_from` lives in `backends/cli.rs` as `pub(crate)` and is called from within +`execute_field_sync` for `Select` fields. It filters the declared options to only those present +in a referenced prior field's value — enabling dependent selects. + +### Legacy Sync Path + +`execute_with_base_dir`, `execute`, and `load_and_execute_from_file` are a pre-`BackendFactory` +sync path gated on `#[cfg(feature = "cli")]`. They call `InquireBackend::execute_field_sync` +directly and bypass the async executor. Available for backward compatibility; the canonical path +for new code is `execute_with_backend_two_phase_with_defaults`. + +--- + +## Known Technical Debt + +### Unbounded Retry Recursion + +Required-field validation retries via direct recursion in `InquireBackend::execute_field_sync`: + +```rust +if is_required && result.is_empty() { + return self.execute_field_sync(field, previous_results); // no depth limit +} +``` + +A user who repeatedly submits empty input on a required field grows the call stack indefinitely. + +### `RenderContext` Clone Cost + +The executor clones `results` into `context` on every field iteration: + +```rust +context.results = results.clone(); +``` + +For forms with many fields containing large values (editor content, multi-select arrays), each +field execution pays O(n) clone cost over all previous results.