TypeDialog/docs/architecture.md

241 lines
9.4 KiB
Markdown
Raw Normal View History

2026-02-17 14:33:20 +00:00
# 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 forms (Nickel / TOML) + i18n + AI │
│ load_form → BackendFactory → dispatch by feature │
2026-02-17 14:33:20 +00:00
├──────────────┬──────────────┬──────────────────────-┤
2026-02-17 14:39:40 +00:00
│ CLI backend │ TUI backend │ Web backend │
2026-02-17 14:33:20 +00:00
│ (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 Nickel (`.ncl`) or TOML file describes the same form for CLI, TUI, and HTTP. No per-backend code. See [ADR-001](./adr/adr-001-nickel-form-definition.md).
2026-02-17 14:33:20 +00:00
**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.
---
## Form Loading
### `load_form` — Unified Entry Point
All binaries and roundtrip paths call `form_parser::load_form(path)`. Extension determines the loader:
```rust
pub fn load_form(path: impl AsRef<Path>) -> Result<FormDefinition> {
match path.as_ref().extension().and_then(|e| e.to_str()) {
Some("ncl") => load_from_ncl(path),
_ => load_from_file(path), // TOML path, unchanged
}
}
```
`.ncl` forms are loaded via `nickel export --format json` as a subprocess. If the export fails for any reason — contract violation, syntax error, missing import — the process aborts before any interaction begins. TOML forms are deserialized directly via `serde`.
Fragment lazy-loading (`includes:` in TOML groups) is replaced by Nickel native imports. `build_element_list` is now a pure in-memory operation with no I/O. The functions `expand_includes` and `load_fragment_form` remain available in `fragments.rs` as rescue code.
### `when_false` — Conditional Field Output
Fields with a `when:` condition that evaluates to false are skipped by default. `when_false = "default"` instructs the executor to inject the field's `default` value into results even when the condition is false:
```toml
[fields.tls_cert_path]
when = "tls_enabled == true"
when_false = "default"
default = "/etc/ssl/cert.pem"
```
This ensures Nickel roundtrip writers always have a value for every schema field regardless of which branches the user navigated.
---
2026-02-17 14:33:20 +00:00
## 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<Value>;
// 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<HashMap<String, Value>>;
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<dyn FormBackend>`. 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<Box<dyn FormBackend>> {
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<String, Value>, // accumulated field values
pub locale: Option<String>, // 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 Phase 2.
This ensures conditional branches are known before iterating the element list.
2026-02-17 14:33:20 +00:00
```rust
let selector_field_names = identify_selector_fields(&form);
// execute only selectors → populate results
```
**Phase 2 — Build element list (pure)**
2026-02-17 14:33:20 +00:00
With Phase 1 results known, the element list is built from the fully-composed in-memory form.
No I/O occurs here — fragments were resolved by `load_form` before execution began.
2026-02-17 14:33:20 +00:00
```rust
let element_list = build_element_list(&form);
2026-02-17 14:33:20 +00:00
```
**Phase 3 — Execute remaining fields**
Iterates the element list, evaluates `when:` conditions per-element, applies `when_false` defaults
for skipped fields, and dispatches to the backend. Two sub-modes:
2026-02-17 14:33:20 +00:00
- `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
├── form_parser::load_form(path) → FormDefinition (.ncl via nickel export | .toml via serde)
2026-02-17 14:33:20 +00:00
├── BackendFactory::create(BackendType::Cli) → Box<dyn FormBackend>
├── 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() — pure, no I/O
│ └── Phase 3: iterate → when/when_false → render_display_item() / execute_field()
2026-02-17 14:33:20 +00:00
└── HashMap<String, Value> → 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<String, Value>,
) -> Result<Value>
```
The `FormBackend::execute_field` trait impl wraps it:
```rust
async fn execute_field(&self, field: &FieldDefinition, context: &RenderContext) -> Result<Value> {
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
refactor(core): eliminate field execution duplication, fix stack-unsafe retry, O(1) context passing Deduplication - Consolidate three copies of match field.field_type into InquireBackend::execute_field_sync as single canonical implementation (backends/cli.rs) - Add previous_results param to execute_field_sync (pub(crate)); propagate options_from filtering previously absent from the cli.rs copy - Move filter_options_from to backends/cli.rs pub(crate); remove duplicate copies from executor.rs and nickel/roundtrip.rs (-346 lines, 0 behavior regressions) - Gate legacy sync path (execute_with_base_dir, execute, load_and_execute_from_file) on #[cfg(feature = "cli")] Retry fix - Replace unbounded recursive retry in execute_field_sync with loop+continue (O(1) stack) RenderContext clone cost - Change RenderContext.results from HashMap<String, Value> to Arc<HashMap<String, Value>> - Change executor accumulators to Arc in execute_with_backend_complete, execute_with_backend_tw execute_with_backend_tw execute_with_backend_tw execute_with_backend_tw execute_with_bante execute_won: execute_with_backend_tw execute_with_backend_tw execute_with_backend_tw execute_count==1 (guaranteed at each insert site) - typedialog-ai/backend.rs: &context.results -> context.results.iter() for Arc deref Doctests - Fix five broken doctests: FormDefinition fields renamed, confirm() arity, FormBackend methods replaced, cli_loader ignore -> no_run with concr methods replaced, cli_loader ignore -> no_run with concr methods reken methods replaced, cli_loader ignore -> no_run with concr methods replaced, clsync path - docs/architecture.md Known Technical Debt: all items resolved, section closed EOF
2026-02-17 15:49:28 +00:00
No unresolved items.