datacraft-core 0.1.3

Headless domain model, command bus and application state for DataCraft
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
//! Serializable state snapshot (step 4.2, item H14; ARCHITECTURE §5).
//!
//! `Core::inspect` projects the application state into this snapshot —
//! the seam headless tests and agents assert on. Test IDs are composed
//! per the §5 convention; every identifier here is derived, never
//! invented.

use std::collections::BTreeMap;
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

use crate::ids::TabId;
use crate::port::DatabaseFormat;
use crate::state::AppState;

/// Snapshot of everything the UI shows.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UiStateSnapshot {
    /// The open database, if any.
    pub database: Option<DatabaseSnapshot>,
    /// Navigation pane visibility.
    pub nav_pane_visible: bool,
    /// Open tabs in opening order.
    pub tabs: Vec<TabSnapshot>,
    /// The active tab, if any.
    pub active_tab: Option<TabId>,
    /// Whether the relationship mapper is open.
    pub relationship_mapper_open: bool,
    /// Whether the SQL window is open (step 13.3, H37).
    pub sql_window_open: bool,
    /// Whether the Import File window is open (step 58.1, item M50).
    pub import_window_open: bool,
    /// The Import File window's parsed preview, if a source was loaded
    /// (step 58.1, item M50).
    pub import_preview: Option<crate::import::ImportPreview>,
    /// Whether the expression window is open (step 41.2, item M46).
    pub expression_window_open: bool,
    /// The shipped database templates (phase 29, item H44) — the registry
    /// behind `CreateDatabaseFromTemplate`, visible to every client.
    pub templates: Vec<crate::templates::TemplateSummary>,
    /// The composition's driver inventory (step 36.4, M23): every driver
    /// the composed binary can open with id, fixed family and offline
    /// capability summary — additive next to `templates`, so agents
    /// pre-flight an open against the reported families.
    pub drivers: Vec<crate::port::DriverInfo>,
    /// Row-write undo depth of the open database (step 28.1, item U14);
    /// `0` without a database.
    pub undo_depth: usize,
    /// Row-write redo depth of the open database (step 28.1, item U14).
    pub redo_depth: usize,
}

/// Snapshot of the open database.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DatabaseSnapshot {
    /// Database name (file stem).
    pub name: String,
    /// Filesystem path of the database file.
    pub path: PathBuf,
    /// The source spec the session was opened with (step 65.2, item L8):
    /// `None` for plain path opens; clients render it beside `path`
    /// (wire-additive — older snapshot JSON without the key keeps
    /// deserializing to `None`).
    #[serde(default)]
    pub source: Option<crate::command::SourceSpec>,
    /// Binary format family.
    pub format: DatabaseFormat,
    /// Identity of the driver serving this source (phase 30, M17;
    /// ADR-7) — GUI, CLI and MCP agents see the same value.
    pub driver: crate::port::DriverId,
    /// The driver's per-session capabilities (phase 30, M17; ADR-7).
    pub capabilities: crate::port::DriverCapabilities,
    /// Disabled command affordances for this source with their reasons
    /// (phase 30, M17; ADR-7): absence means enabled, so capable sources
    /// carry an empty map. UI code renders from this — never from format
    /// matches.
    pub availability: BTreeMap<String, crate::availability::DisabledReason>,
    /// Tables in catalog order, with their navigation-pane test IDs.
    pub tables: Vec<TableEntrySnapshot>,
    /// Referential relationships (mapper pane and agents). System rows
    /// (`MSys*`) are filtered at load — they are Access's internal
    /// metadata, not user relationships (step 69.1, H57).
    pub relationships: Vec<crate::port::Relationship>,
    /// Query definitions in creation order.
    pub queries: Vec<QueryEntrySnapshot>,
    /// Sidecar view designs in creation order (step 71.1, item M93).
    pub views: Vec<ViewEntrySnapshot>,
    /// File-owned views of the opened source (phase 73, step 73.1; M90) —
    /// read-only; sidecar designs with the same name are not repeated.
    #[serde(default)]
    pub native_views: Vec<NativeViewSnapshot>,
    /// Indexes the opened source carries (phase 73, step 73.2; M90) —
    /// read-only inventory.
    #[serde(default)]
    pub indexes: Vec<crate::port::SourceIndex>,
    /// Triggers the opened source carries (phase 73, step 73.2; M90) —
    /// inventory only.
    #[serde(default)]
    pub triggers: Vec<crate::port::SourceTrigger>,
    /// Driver facts of the opened file (phase 73, step 73.3; M91).
    #[serde(default)]
    pub facts: Vec<crate::port::SourceFact>,
    /// The file's health verdict: `None` = passed or not checked,
    /// `Some(reason)` = the bounded check reported damage (M91).
    #[serde(default)]
    pub health: Option<String>,
    /// Native Access forms/reports named by the catalog (phase 74, step
    /// 74.1; M87) — read-only, definitions never decoded.
    #[serde(default)]
    pub native_objects: Vec<NativeObjectSnapshot>,
    /// Why the native-query decode failed, if it did (step 23.3, M10).
    pub native_queries_error: Option<String>,
    /// Form design documents in creation order.
    pub forms: Vec<crate::form::FormDef>,
    /// Report design documents in creation order (step 10.4, H31).
    pub reports: Vec<crate::report::ReportDef>,
    /// Named macros in catalog order (step 17.1, L4; read-only inventory).
    pub macros: Vec<MacroEntrySnapshot>,
    /// Why the macro inventory could not be decoded, if it failed.
    pub macros_error: Option<String>,
    /// Extracted VBA project summary (step 17.2, L5); `None` when the
    /// database has no decoded project.
    pub vba: Option<VbaProjectSnapshot>,
    /// Why the VBA extraction failed, if it did.
    pub vba_error: Option<String>,
    /// Why the sibling `.datacraft` sidecar did not auto-load (phase 29,
    /// step 29.4); absent for a missing sidecar.
    pub sidecar_error: Option<String>,
    /// A sibling Access lock file detected at open (step 53.1, item M59):
    /// the file NAME as data — the session opened read-only. Clients
    /// localize the notice from this field (ADR-4); absent when no lock
    /// file exists beside the database.
    pub lock_file: Option<String>,
}

/// One native Access form/report in the snapshot (phase 74, step 74.1;
/// item M87). Read-only: no tab, no definition bytes.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NativeObjectSnapshot {
    /// Object name as the catalog reports it.
    pub name: String,
    /// Form or report.
    pub kind: crate::port::NativeObjectKind,
    /// `navpane.<expanded|collapsed>.native.<kind>.<name>`.
    pub ui_test_id: String,
}

/// One file-owned (native) view in the snapshot (phase 73, step 73.1;
/// item M90). Read-only: no design tab, no write affordance.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NativeViewSnapshot {
    /// View name as the file reports it.
    pub name: String,
    /// The source statement verbatim (the tab's boundary hint).
    pub sql: String,
    /// `navpane.<expanded|collapsed>.node.view.<name>`.
    pub ui_test_id: String,
}

/// Snapshot of the extracted VBA project (module tree only — source is
/// fetched per module through `LoadVbaModule`, never mirrored here).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VbaProjectSnapshot {
    /// Modules in `dir`-stream order.
    pub modules: Vec<VbaModuleSnapshot>,
}

/// One VBA module in the snapshot.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VbaModuleSnapshot {
    /// Module name as shown in the navigation pane and viewer.
    pub name: String,
    /// `navpane.<expanded|collapsed>.node.module.<name>`.
    pub ui_test_id: String,
    /// Module kind.
    pub kind: crate::vba::VbaModuleKind,
    /// Source line count (the viewer's summary).
    pub lines: usize,
}

/// One macro in the snapshot (navigation pane entry and viewer input).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MacroEntrySnapshot {
    /// Macro name as shown in the navigation pane.
    pub name: String,
    /// `navpane.<expanded|collapsed>.node.macro.<name>`.
    pub ui_test_id: String,
    /// Readable definition arguments in storage order.
    pub actions: Vec<crate::macro_def::MacroAction>,
    /// Whether the definition blob was decoded.
    pub definition_decoded: bool,
}

/// One query in the snapshot.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueryEntrySnapshot {
    /// Query name.
    pub name: String,
    /// Source table name.
    pub table: String,
    /// `navpane.<expanded|collapsed>.node.query.<name>`.
    pub ui_test_id: String,
    /// QBE columns.
    pub fields: Vec<crate::qbe::QbeField>,
}

/// One sidecar view in the snapshot (step 71.1, item M93).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewEntrySnapshot {
    /// View name.
    pub name: String,
    /// The SELECT statement (agents and the view tab execute it).
    pub sql: String,
    /// `navpane.<expanded|collapsed>.node.view.<name>`.
    pub ui_test_id: String,
}

/// One field of a table in the snapshot.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FieldSnapshot {
    /// Field name.
    pub name: String,
    /// Domain kind (designer field-list mapping and agents).
    pub kind: crate::schema::FieldKind,
    /// Access UI type name (design grid Data Type column).
    pub kind_display: String,
    /// Full field properties (design grid property sheet).
    pub properties: crate::schema::FieldProperties,
}

/// One table in the snapshot (navigation pane entry).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TableEntrySnapshot {
    /// Table name as shown in the navigation pane.
    pub name: String,
    /// `navpane.<expanded|collapsed>.<ObjectId::ui_test_id()>`.
    pub ui_test_id: String,
    /// Fields in storage order (datasheet headers render from this).
    pub fields: Vec<FieldSnapshot>,
}

/// Snapshot of one open tab.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TabSnapshot {
    /// Tab id (stable while the tab is open).
    pub id: TabId,
    /// Object kind slug (`"table"`, `"query"`, …).
    pub object_kind: String,
    /// Object name as shown in the tab bar.
    pub object_name: String,
    /// `workspace.tab.<kind>.<name>`.
    pub ui_test_id: String,
    /// Current view mode (step 21.1, item U5).
    pub mode: crate::state::TabMode,
    /// Whether the tab has a second (design) view (step 21.1, item U5).
    pub has_design_view: bool,
    /// Datasheet sort field of a table tab (step 44.5, item M54).
    pub sort_field: Option<String>,
    /// Datasheet sort direction of a table tab (step 44.5, item M54).
    pub sort_order: Option<crate::qbe::SortOrder>,
    /// Datasheet column filters of a table tab (step 44.5, item M54):
    /// field name → criteria text.
    pub filters: std::collections::BTreeMap<String, String>,
}

impl UiStateSnapshot {
    /// Projects the application state.
    pub fn of(state: &AppState) -> Self {
        Self {
            templates: crate::templates::summaries(),
            drivers: state.drivers.clone(),
            database: state.database.as_ref().map(|db| DatabaseSnapshot {
                name: db.name.clone(),
                path: db.path.clone(),
                source: db.source.clone(),
                format: db.format,
                driver: db.driver.clone(),
                capabilities: db.capabilities,
                availability: crate::availability::affordances(&db.capabilities),
                relationships: db.relationships.clone(),
                forms: db.forms.clone(),
                reports: db.reports.clone(),
                macros: db
                    .macros
                    .iter()
                    .map(|macro_def| MacroEntrySnapshot {
                        name: macro_def.name.clone(),
                        ui_test_id: state.nav_pane_test_id(&crate::ObjectId::new(
                            crate::ObjectKind::Macro,
                            &macro_def.name,
                        )),
                        actions: macro_def.actions.clone(),
                        definition_decoded: macro_def.definition_decoded,
                    })
                    .collect(),
                macros_error: db.macros_error.clone(),
                native_queries_error: db.native_queries_error.clone(),
                vba: db.vba_project.as_ref().map(|project| VbaProjectSnapshot {
                    modules: project
                        .modules
                        .iter()
                        .map(|module| VbaModuleSnapshot {
                            name: module.name.clone(),
                            ui_test_id: state.nav_pane_test_id(&crate::ObjectId::new(
                                crate::ObjectKind::Module,
                                &module.name,
                            )),
                            kind: module.kind,
                            lines: module.lines(),
                        })
                        .collect(),
                }),
                vba_error: db.vba_error.clone(),
                sidecar_error: db.sidecar_error.clone(),
                lock_file: db.lock_file.clone(),
                queries: db
                    .queries
                    .iter()
                    .map(|q| QueryEntrySnapshot {
                        name: q.name.clone(),
                        table: q.table.clone(),
                        ui_test_id: state.nav_pane_test_id(&crate::ObjectId::new(
                            crate::ObjectKind::Query,
                            &q.name,
                        )),
                        fields: q.fields.clone(),
                    })
                    .chain(db.native_queries.iter().map(|query| QueryEntrySnapshot {
                        name: query.name.clone(),
                        table: query.tables.first().cloned().unwrap_or_default(),
                        ui_test_id: state.nav_pane_test_id(&crate::ObjectId::new(
                            crate::ObjectKind::Query,
                            &query.name,
                        )),
                        fields: query.query_def().map(|def| def.fields).unwrap_or_default(),
                    }))
                    .collect(),
                indexes: db.indexes.clone(),
                triggers: db.triggers.clone(),
                facts: db.source_facts.facts.clone(),
                health: db.source_facts.health.clone(),
                native_objects: db
                    .native_objects
                    .iter()
                    .map(|object| NativeObjectSnapshot {
                        name: object.name.clone(),
                        kind: object.kind,
                        ui_test_id: format!(
                            "navpane.{}.native.{}.{}",
                            if state.nav_pane_visible {
                                "expanded"
                            } else {
                                "collapsed"
                            },
                            object.kind.slug(),
                            object.name.to_lowercase()
                        ),
                    })
                    .collect(),
                native_views: db
                    .native_views
                    .iter()
                    .map(|view| NativeViewSnapshot {
                        name: view.name.clone(),
                        sql: view.sql.clone(),
                        ui_test_id: state.nav_pane_test_id(&crate::ObjectId::new(
                            crate::ObjectKind::View,
                            &view.name,
                        )),
                    })
                    .collect(),
                views: db
                    .views
                    .iter()
                    .map(|view| ViewEntrySnapshot {
                        name: view.name.clone(),
                        sql: view.sql.clone(),
                        ui_test_id: state.nav_pane_test_id(&crate::ObjectId::new(
                            crate::ObjectKind::View,
                            &view.name,
                        )),
                    })
                    .collect(),
                tables: db
                    .tables
                    .iter()
                    .map(|t| TableEntrySnapshot {
                        name: t.name.clone(),
                        ui_test_id: state.nav_pane_test_id(&crate::ObjectId::new(
                            crate::ObjectKind::Table,
                            &t.name,
                        )),
                        fields: t
                            .fields
                            .iter()
                            .map(|f| FieldSnapshot {
                                name: f.name.clone(),
                                kind: f.kind,
                                kind_display: crate::present::field_kind_display(&f.kind),
                                properties: f.properties.clone(),
                            })
                            .collect(),
                    })
                    .collect(),
            }),
            nav_pane_visible: state.nav_pane_visible,
            tabs: state
                .tabs
                .iter()
                .map(|tab| {
                    let view = state
                        .database
                        .as_ref()
                        .and_then(|db| db.table_views.get(&tab.object.name))
                        .filter(|_| tab.object.kind == crate::ObjectKind::Table);
                    TabSnapshot {
                        id: tab.id,
                        object_kind: tab.object.kind.slug().to_owned(),
                        object_name: tab.object.name.clone(),
                        ui_test_id: tab.ui_test_id(),
                        mode: tab.mode,
                        has_design_view: tab.object.kind.has_design_view(),
                        sort_field: view.and_then(|view| view.sort_field.clone()),
                        sort_order: view.and_then(|view| view.sort_order),
                        filters: view.map(|view| view.filters.clone()).unwrap_or_default(),
                    }
                })
                .collect(),
            active_tab: state.active_tab,
            relationship_mapper_open: state.relationship_mapper_open,
            sql_window_open: state.sql_window_open,
            import_window_open: state.import_window_open,
            import_preview: state.import_preview.clone(),
            expression_window_open: state.expression_window_open,
            undo_depth: state.database.as_ref().map_or(0, |db| db.undo.len()),
            redo_depth: state.database.as_ref().map_or(0, |db| db.redo.len()),
        }
    }

    /// Serializes deterministically (pretty JSON, stable field order).
    ///
    /// Infallible for this snapshot: no non-string map keys or other serde
    /// failure sources.
    pub fn to_json(&self) -> String {
        serde_json::to_string_pretty(self).expect("UiStateSnapshot serialization is infallible")
    }
}