Skip to main content

spreadsheet_kit/model/
mod.rs

1use crate::caps::BackendCaps;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6pub mod diagnostics;
7pub use diagnostics::*;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, Default)]
10#[serde(transparent)]
11pub struct WorkbookId(pub String);
12
13impl WorkbookId {
14    pub fn as_str(&self) -> &str {
15        &self.0
16    }
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
20pub struct Warning {
21    pub code: String,
22    pub message: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
26pub struct WorkbookDescriptor {
27    pub workbook_id: WorkbookId,
28    pub short_id: String,
29    pub slug: String,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub folder: Option<String>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub path: Option<String>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub client_path: Option<String>,
36    pub bytes: u64,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub last_modified: Option<String>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub revision_id: Option<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub caps: Option<BackendCaps>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
46pub struct WorkbookListResponse {
47    pub workbooks: Vec<WorkbookDescriptor>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub next_offset: Option<u32>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
53pub struct WorkbookDescription {
54    pub workbook_id: WorkbookId,
55    pub short_id: String,
56    pub slug: String,
57    pub path: String,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub client_path: Option<String>,
60    pub bytes: u64,
61    pub sheet_count: usize,
62    pub defined_names: usize,
63    pub tables: usize,
64    pub macros_present: bool,
65    pub last_modified: Option<String>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub revision_id: Option<String>,
68    pub caps: BackendCaps,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
72pub struct WorkbookSummaryResponse {
73    pub workbook_id: WorkbookId,
74    pub slug: String,
75    pub sheet_count: usize,
76    pub total_cells: u64,
77    pub total_formulas: u64,
78    pub breakdown: WorkbookBreakdown,
79    pub region_counts: RegionCountSummary,
80    #[serde(skip_serializing_if = "Vec::is_empty")]
81    pub key_named_ranges: Vec<NamedRangeDescriptor>,
82    #[serde(skip_serializing_if = "Vec::is_empty")]
83    pub suggested_entry_points: Vec<EntryPoint>,
84    #[serde(skip_serializing_if = "Vec::is_empty")]
85    pub notes: Vec<String>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
89pub struct WorkbookBreakdown {
90    pub data_sheets: u32,
91    pub calculator_sheets: u32,
92    pub parameter_sheets: u32,
93    pub metadata_sheets: u32,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
97pub struct RegionCountSummary {
98    pub data: u32,
99    pub parameters: u32,
100    pub outputs: u32,
101    pub calculator: u32,
102    pub metadata: u32,
103    pub other: u32,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
107pub struct EntryPoint {
108    pub sheet_name: String,
109    pub region_id: Option<u32>,
110    pub bounds: Option<String>,
111    pub rationale: String,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
115pub struct SheetSummary {
116    pub name: String,
117    pub visible: bool,
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub row_count: Option<u32>,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub column_count: Option<u32>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub non_empty_cells: Option<u32>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub formula_cells: Option<u32>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub cached_values: Option<u32>,
128    pub classification: SheetClassification,
129    #[serde(skip_serializing_if = "Vec::is_empty")]
130    pub style_tags: Vec<String>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
134#[serde(rename_all = "snake_case")]
135pub enum SheetClassification {
136    Data,
137    Calculator,
138    Mixed,
139    Metadata,
140    Empty,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
144pub struct SheetListResponse {
145    pub workbook_id: WorkbookId,
146    pub sheets: Vec<SheetSummary>,
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub next_offset: Option<u32>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
152pub struct SheetOverviewResponse {
153    pub workbook_id: WorkbookId,
154    pub sheet_name: String,
155    pub narrative: String,
156    pub regions: Vec<SheetRegion>,
157    pub detected_regions: Vec<DetectedRegion>,
158    pub detected_region_count: u32,
159    pub detected_regions_truncated: bool,
160    pub key_ranges: Vec<String>,
161    pub formula_ratio: f32,
162    pub notable_features: Vec<String>,
163    pub notes: Vec<String>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
167pub struct SheetRegion {
168    pub kind: RegionKind,
169    pub address: String,
170    pub description: String,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
174pub enum RegionKind {
175    #[serde(rename = "likely_table")]
176    Table,
177    #[serde(rename = "likely_data")]
178    Data,
179    #[serde(rename = "likely_parameters")]
180    Parameters,
181    #[serde(rename = "likely_outputs")]
182    Outputs,
183    #[serde(rename = "likely_calculator")]
184    Calculator,
185    #[serde(rename = "likely_metadata")]
186    Metadata,
187    #[serde(rename = "likely_styles")]
188    Styles,
189    #[serde(rename = "likely_comments")]
190    Comments,
191    #[serde(rename = "unknown")]
192    Other,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
196pub struct DetectedRegion {
197    pub id: u32,
198    pub bounds: String,
199    pub header_row: Option<u32>,
200    pub headers: Vec<String>,
201    pub header_count: u32,
202    pub headers_truncated: bool,
203    pub row_count: u32,
204    pub classification: RegionKind,
205    pub region_kind: Option<RegionKind>,
206    pub confidence: f32,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
210pub struct SheetPageResponse {
211    pub workbook_id: WorkbookId,
212    pub sheet_name: String,
213    #[serde(skip_serializing_if = "Vec::is_empty")]
214    pub rows: Vec<RowSnapshot>,
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub next_start_row: Option<u32>,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub header_row: Option<RowSnapshot>,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub compact: Option<SheetPageCompact>,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub values_only: Option<SheetPageValues>,
223    pub format: SheetPageFormat,
224    /// True when the response was truncated by cell/payload budget limits.
225    #[serde(default, skip_serializing_if = "is_false")]
226    pub truncated: bool,
227    /// Machine-consumable budget/continuation metadata.
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub budget: Option<ReadBudget>,
230}
231
232/// Machine-consumable output-budget metadata attached to read-surface responses.
233///
234/// Allows agents to detect truncation deterministically and build continuation
235/// requests without guessing.
236#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
237pub struct ReadBudget {
238    /// Maximum cells allowed in a single response.
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub max_cells: Option<usize>,
241    /// Maximum payload bytes allowed in a single response.
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub max_payload_bytes: Option<usize>,
244    /// Number of rows actually returned.
245    pub rows_returned: usize,
246    /// Number of cells actually returned.
247    pub cells_returned: usize,
248    /// Total rows available in the queried range (if known).
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub total_rows_available: Option<u32>,
251    /// Human/agent-readable continuation hint (e.g. "use start_row=51 to continue").
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub continuation: Option<String>,
254}
255
256fn is_false(v: &bool) -> bool {
257    !v
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
261pub struct RowSnapshot {
262    pub row_index: u32,
263    pub cells: Vec<CellSnapshot>,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
267pub struct CellSnapshot {
268    pub address: String,
269    pub value: Option<CellValue>,
270    pub formula: Option<String>,
271    pub cached_value: Option<CellValue>,
272    pub number_format: Option<String>,
273    pub style_tags: Vec<String>,
274    pub notes: Vec<String>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
278#[serde(tag = "kind", content = "value")]
279pub enum CellValue {
280    Text(String),
281    Number(f64),
282    Bool(bool),
283    Error(String),
284    Date(String),
285}
286
287#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
288#[serde(rename_all = "snake_case")]
289pub enum CellValueKind {
290    Text,
291    Number,
292    Bool,
293    Error,
294    Date,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
298#[serde(untagged)]
299pub enum CellValuePrimitive {
300    Text(String),
301    Number(f64),
302    Bool(bool),
303}
304
305#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
306#[serde(rename_all = "snake_case")]
307pub enum TableOutputFormat {
308    Json,
309    Values,
310    Csv,
311    Dense,
312    Rows,
313}
314
315#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default)]
316#[serde(rename_all = "snake_case")]
317pub enum SheetPageFormat {
318    #[default]
319    Full,
320    Compact,
321    ValuesOnly,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
325pub struct SheetPageCompact {
326    pub headers: Vec<String>,
327    pub header_row: Vec<Option<CellValue>>,
328    pub rows: Vec<Vec<Option<CellValue>>>,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
332pub struct SheetPageValues {
333    pub rows: Vec<Vec<Option<CellValue>>>,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
337pub struct SheetStatisticsResponse {
338    pub workbook_id: WorkbookId,
339    pub sheet_name: String,
340    pub row_count: u32,
341    pub column_count: u32,
342    pub density: f32,
343    #[serde(skip_serializing_if = "Vec::is_empty")]
344    pub numeric_columns: Vec<ColumnSummary>,
345    #[serde(skip_serializing_if = "Vec::is_empty")]
346    pub text_columns: Vec<ColumnSummary>,
347    pub null_counts: BTreeMap<String, u32>,
348    pub duplicate_warnings: Vec<String>,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
352pub struct ColumnSummary {
353    pub header: Option<String>,
354    pub column: String,
355    #[serde(skip_serializing_if = "Vec::is_empty")]
356    pub samples: Vec<CellValue>,
357    pub min: Option<f64>,
358    pub max: Option<f64>,
359    pub mean: Option<f64>,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
363pub struct SheetFormulaMapResponse {
364    pub workbook_id: WorkbookId,
365    pub sheet_name: String,
366    pub groups: Vec<FormulaGroup>,
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub formula_parse_diagnostics: Option<FormulaParseDiagnostics>,
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub next_offset: Option<u32>,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
374pub struct FormulaGroup {
375    pub fingerprint: String,
376    #[serde(skip_serializing_if = "Vec::is_empty")]
377    pub addresses: Vec<String>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub count: Option<u32>,
380    pub formula: String,
381    pub is_array: bool,
382    pub is_shared: bool,
383    pub is_volatile: bool,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
387pub struct FormulaTraceResponse {
388    pub workbook_id: WorkbookId,
389    pub sheet_name: String,
390    pub origin: String,
391    pub direction: TraceDirection,
392    pub layers: Vec<TraceLayer>,
393    pub next_cursor: Option<TraceCursor>,
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub formula_parse_diagnostics: Option<FormulaParseDiagnostics>,
396    pub notes: Vec<String>,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
400pub struct FormulaTraceEdge {
401    pub from: String,
402    pub to: String,
403    pub formula: Option<String>,
404    pub note: Option<String>,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
408pub struct TraceLayer {
409    pub depth: u32,
410    pub summary: TraceLayerSummary,
411    pub highlights: TraceLayerHighlights,
412    pub edges: Vec<FormulaTraceEdge>,
413    pub has_more: bool,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
417pub struct TraceLayerSummary {
418    pub total_nodes: usize,
419    pub formula_nodes: usize,
420    pub value_nodes: usize,
421    pub blank_nodes: usize,
422    pub external_nodes: usize,
423    pub unique_formula_groups: usize,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
427pub struct TraceLayerHighlights {
428    pub top_ranges: Vec<TraceRangeHighlight>,
429    pub top_formula_groups: Vec<TraceFormulaGroupHighlight>,
430    pub notable_cells: Vec<TraceCellHighlight>,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
434pub struct TraceRangeHighlight {
435    pub start: String,
436    pub end: String,
437    pub count: usize,
438    pub literals: usize,
439    pub formulas: usize,
440    pub blanks: usize,
441    pub sample_values: Vec<CellValue>,
442    pub sample_formulas: Vec<String>,
443    pub sample_addresses: Vec<String>,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
447pub struct TraceFormulaGroupHighlight {
448    pub fingerprint: String,
449    pub formula: String,
450    pub count: usize,
451    pub sample_addresses: Vec<String>,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
455pub struct TraceCellHighlight {
456    pub address: String,
457    pub kind: TraceCellKind,
458    pub value: Option<CellValue>,
459    pub formula: Option<String>,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash)]
463#[serde(rename_all = "snake_case")]
464pub enum TraceCellKind {
465    Formula,
466    Literal,
467    Blank,
468    External,
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
472pub struct TraceCursor {
473    pub depth: u32,
474    pub offset: usize,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
478#[serde(rename_all = "snake_case")]
479pub enum TraceDirection {
480    Precedents,
481    Dependents,
482}
483
484#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
485#[serde(rename_all = "snake_case")]
486pub enum NamedRangeScope {
487    Workbook,
488    Sheet,
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
492pub struct NamedRangeDescriptor {
493    pub name: String,
494    pub scope: Option<String>,
495    /// Explicit scope kind: "workbook" or "sheet".
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub scope_kind: Option<NamedRangeScope>,
498    /// Sheet name when scope_kind is "sheet".
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub scope_sheet_name: Option<String>,
501    pub refers_to: String,
502    pub kind: NamedItemKind,
503    pub sheet_name: Option<String>,
504    pub comment: Option<String>,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
508#[serde(rename_all = "snake_case")]
509pub enum NamedItemKind {
510    NamedRange,
511    Table,
512    Formula,
513    Unknown,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
517pub struct NamedRangesResponse {
518    pub workbook_id: WorkbookId,
519    pub items: Vec<NamedRangeDescriptor>,
520}
521
522#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
523pub struct DefineNameResponse {
524    pub workbook_id: WorkbookId,
525    pub name: String,
526    pub refers_to: String,
527    pub scope_kind: NamedRangeScope,
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub scope_sheet_name: Option<String>,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
533pub struct UpdateNameResponse {
534    pub workbook_id: WorkbookId,
535    pub name: String,
536    pub refers_to: String,
537    pub scope_kind: NamedRangeScope,
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub scope_sheet_name: Option<String>,
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub previous_refers_to: Option<String>,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
545pub struct DeleteNameResponse {
546    pub workbook_id: WorkbookId,
547    pub name: String,
548    pub deleted: bool,
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
552pub struct FindFormulaMatch {
553    pub address: String,
554    pub sheet_name: String,
555    pub formula: String,
556    pub cached_value: Option<CellValue>,
557    pub context: Vec<RowSnapshot>,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
561pub struct FindFormulaResponse {
562    pub workbook_id: WorkbookId,
563    pub matches: Vec<FindFormulaMatch>,
564    #[serde(skip_serializing_if = "Option::is_none")]
565    pub next_offset: Option<u32>,
566}
567
568#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
569pub struct VolatileScanEntry {
570    pub address: String,
571    pub sheet_name: String,
572    pub function: String,
573    pub note: Option<String>,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
577pub struct VolatileScanResponse {
578    pub workbook_id: WorkbookId,
579    pub items: Vec<VolatileScanEntry>,
580    #[serde(skip_serializing_if = "Option::is_none")]
581    pub formula_parse_diagnostics: Option<FormulaParseDiagnostics>,
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub next_offset: Option<u32>,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
587pub struct StyleDescriptor {
588    pub font: Option<FontDescriptor>,
589    pub fill: Option<FillDescriptor>,
590    pub borders: Option<BordersDescriptor>,
591    pub alignment: Option<AlignmentDescriptor>,
592    pub number_format: Option<String>,
593}
594
595#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
596pub struct FontDescriptor {
597    pub name: Option<String>,
598    pub size: Option<f64>,
599    pub bold: Option<bool>,
600    pub italic: Option<bool>,
601    pub underline: Option<String>,
602    pub strikethrough: Option<bool>,
603    pub color: Option<String>,
604}
605
606#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
607#[serde(tag = "kind", rename_all = "snake_case")]
608pub enum FillDescriptor {
609    Pattern(PatternFillDescriptor),
610    Gradient(GradientFillDescriptor),
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
614pub struct PatternFillDescriptor {
615    pub pattern_type: Option<String>,
616    pub foreground_color: Option<String>,
617    pub background_color: Option<String>,
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
621pub struct GradientFillDescriptor {
622    pub degree: Option<f64>,
623    pub stops: Vec<GradientStopDescriptor>,
624}
625
626#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
627pub struct GradientStopDescriptor {
628    pub position: f64,
629    pub color: String,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
633pub struct BordersDescriptor {
634    pub left: Option<BorderSideDescriptor>,
635    pub right: Option<BorderSideDescriptor>,
636    pub top: Option<BorderSideDescriptor>,
637    pub bottom: Option<BorderSideDescriptor>,
638    pub diagonal: Option<BorderSideDescriptor>,
639    pub vertical: Option<BorderSideDescriptor>,
640    pub horizontal: Option<BorderSideDescriptor>,
641    pub diagonal_up: Option<bool>,
642    pub diagonal_down: Option<bool>,
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
646pub struct BorderSideDescriptor {
647    pub style: Option<String>,
648    pub color: Option<String>,
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
652pub struct AlignmentDescriptor {
653    pub horizontal: Option<String>,
654    pub vertical: Option<String>,
655    pub wrap_text: Option<bool>,
656    pub text_rotation: Option<u32>,
657}
658
659// Patch variants for write tools (Phase 2+). Double-option fields distinguish:
660// - missing field => no change (merge mode)
661// - null => clear to default
662// - value => set/merge that value
663#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
664pub struct StylePatch {
665    #[serde(default)]
666    pub font: Option<Option<FontPatch>>,
667    #[serde(default)]
668    pub fill: Option<Option<FillPatch>>,
669    #[serde(default)]
670    pub borders: Option<Option<BordersPatch>>,
671    #[serde(default)]
672    pub alignment: Option<Option<AlignmentPatch>>,
673    #[serde(default)]
674    pub number_format: Option<Option<String>>,
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
678pub struct FontPatch {
679    #[serde(default)]
680    pub name: Option<Option<String>>,
681    #[serde(default)]
682    pub size: Option<Option<f64>>,
683    #[serde(default)]
684    pub bold: Option<Option<bool>>,
685    #[serde(default)]
686    pub italic: Option<Option<bool>>,
687    #[serde(default)]
688    pub underline: Option<Option<String>>,
689    #[serde(default)]
690    pub strikethrough: Option<Option<bool>>,
691    #[serde(default)]
692    pub color: Option<Option<String>>,
693}
694
695#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
696#[serde(tag = "kind", rename_all = "snake_case")]
697pub enum FillPatch {
698    Pattern(PatternFillPatch),
699    Gradient(GradientFillPatch),
700}
701
702#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
703pub struct PatternFillPatch {
704    #[serde(default)]
705    pub pattern_type: Option<Option<String>>,
706    #[serde(default)]
707    pub foreground_color: Option<Option<String>>,
708    #[serde(default)]
709    pub background_color: Option<Option<String>>,
710}
711
712#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
713pub struct GradientFillPatch {
714    #[serde(default)]
715    pub degree: Option<Option<f64>>,
716    #[serde(default)]
717    pub stops: Option<Vec<GradientStopPatch>>,
718}
719
720#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
721pub struct GradientStopPatch {
722    pub position: f64,
723    pub color: String,
724}
725
726#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
727pub struct BordersPatch {
728    #[serde(default)]
729    pub left: Option<Option<BorderSidePatch>>,
730    #[serde(default)]
731    pub right: Option<Option<BorderSidePatch>>,
732    #[serde(default)]
733    pub top: Option<Option<BorderSidePatch>>,
734    #[serde(default)]
735    pub bottom: Option<Option<BorderSidePatch>>,
736    #[serde(default)]
737    pub diagonal: Option<Option<BorderSidePatch>>,
738    #[serde(default)]
739    pub vertical: Option<Option<BorderSidePatch>>,
740    #[serde(default)]
741    pub horizontal: Option<Option<BorderSidePatch>>,
742    #[serde(default)]
743    pub diagonal_up: Option<Option<bool>>,
744    #[serde(default)]
745    pub diagonal_down: Option<Option<bool>>,
746}
747
748#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
749pub struct BorderSidePatch {
750    #[serde(default)]
751    pub style: Option<Option<String>>,
752    #[serde(default)]
753    pub color: Option<Option<String>>,
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
757pub struct AlignmentPatch {
758    #[serde(default)]
759    pub horizontal: Option<Option<String>>,
760    #[serde(default)]
761    pub vertical: Option<Option<String>>,
762    #[serde(default)]
763    pub wrap_text: Option<Option<bool>>,
764    #[serde(default)]
765    pub text_rotation: Option<Option<u32>>,
766}
767
768#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
769pub struct SheetStylesResponse {
770    pub workbook_id: WorkbookId,
771    pub sheet_name: String,
772    pub styles: Vec<StyleSummary>,
773    #[serde(skip_serializing_if = "Vec::is_empty")]
774    pub conditional_rules: Vec<String>,
775    pub total_styles: u32,
776    pub styles_truncated: bool,
777}
778
779#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
780pub struct StyleSummary {
781    pub style_id: String,
782    pub occurrences: u32,
783    pub tags: Vec<String>,
784    #[serde(skip_serializing_if = "Vec::is_empty")]
785    pub example_cells: Vec<String>,
786    pub descriptor: Option<StyleDescriptor>,
787    #[serde(skip_serializing_if = "Vec::is_empty")]
788    pub cell_ranges: Vec<String>,
789    pub ranges_truncated: bool,
790}
791
792#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
793pub struct WorkbookStyleSummaryResponse {
794    pub workbook_id: WorkbookId,
795    #[serde(skip_serializing_if = "Option::is_none")]
796    pub theme: Option<ThemeSummary>,
797    #[serde(skip_serializing_if = "Option::is_none")]
798    pub inferred_default_style_id: Option<String>,
799    #[serde(skip_serializing_if = "Option::is_none")]
800    pub inferred_default_font: Option<FontDescriptor>,
801    pub styles: Vec<WorkbookStyleUsage>,
802    pub total_styles: u32,
803    pub styles_truncated: bool,
804    #[serde(skip_serializing_if = "Vec::is_empty")]
805    pub conditional_formats: Vec<ConditionalFormatSummary>,
806    pub conditional_formats_truncated: bool,
807    pub scan_truncated: bool,
808    #[serde(skip_serializing_if = "Vec::is_empty")]
809    pub notes: Vec<String>,
810}
811
812#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
813pub struct WorkbookStyleUsage {
814    pub style_id: String,
815    pub occurrences: u32,
816    pub tags: Vec<String>,
817    #[serde(skip_serializing_if = "Vec::is_empty")]
818    pub example_cells: Vec<String>,
819    pub descriptor: Option<StyleDescriptor>,
820}
821
822#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
823pub struct ThemeSummary {
824    pub name: Option<String>,
825    pub colors: BTreeMap<String, String>,
826    pub font_scheme: ThemeFontSchemeSummary,
827}
828
829#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
830pub struct ThemeFontSchemeSummary {
831    pub major_latin: Option<String>,
832    pub major_east_asian: Option<String>,
833    pub major_complex_script: Option<String>,
834    pub minor_latin: Option<String>,
835    pub minor_east_asian: Option<String>,
836    pub minor_complex_script: Option<String>,
837}
838
839#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
840pub struct ConditionalFormatSummary {
841    pub sheet_name: String,
842    pub range: String,
843    pub rule_types: Vec<String>,
844    pub rule_count: u32,
845}
846
847#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
848pub struct ManifestStubResponse {
849    pub workbook_id: WorkbookId,
850    pub slug: String,
851    pub manifest_yaml: String,
852    pub sheets: Vec<ManifestSheetStub>,
853}
854
855#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
856pub struct ManifestSheetStub {
857    pub sheet_name: String,
858    pub classification: SheetClassification,
859    pub candidate_expectations: Vec<String>,
860    pub notes: Vec<String>,
861}
862
863#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
864#[serde(rename_all = "snake_case")]
865pub enum FindMode {
866    #[default]
867    Value,
868    Label,
869}
870
871#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
872#[serde(rename_all = "snake_case")]
873pub enum LabelDirection {
874    Right,
875    Below,
876    Any,
877}
878
879#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
880pub struct FindValueMatch {
881    pub address: String,
882    pub sheet_name: String,
883    pub value: Option<CellValue>,
884    pub row_context: Option<RowContext>,
885    pub neighbors: Option<NeighborValues>,
886    pub label_hit: Option<LabelHit>,
887}
888
889#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
890pub struct RowContext {
891    pub headers: Vec<String>,
892    pub values: Vec<Option<CellValue>>,
893}
894
895#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
896pub struct NeighborValues {
897    pub left: Option<CellValue>,
898    pub right: Option<CellValue>,
899    pub up: Option<CellValue>,
900    pub down: Option<CellValue>,
901}
902
903#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
904pub struct LabelHit {
905    pub label_address: String,
906    pub label: String,
907}
908
909#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
910pub struct FindValueResponse {
911    pub workbook_id: WorkbookId,
912    pub matches: Vec<FindValueMatch>,
913    pub match_count: u32,
914    #[serde(skip_serializing_if = "Option::is_none")]
915    pub next_offset: Option<u32>,
916}
917
918pub type TableRow = BTreeMap<String, Option<CellValue>>;
919
920#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
921pub struct ReadTableResponse {
922    pub workbook_id: WorkbookId,
923    pub sheet_name: String,
924    pub table_name: Option<String>,
925    #[serde(skip_serializing_if = "Vec::is_empty")]
926    pub warnings: Vec<Warning>,
927    #[serde(skip_serializing_if = "Vec::is_empty")]
928    pub headers: Vec<String>,
929    #[serde(skip_serializing_if = "Vec::is_empty")]
930    pub rows: Vec<TableRow>,
931    #[serde(skip_serializing_if = "Option::is_none")]
932    pub values: Option<Vec<Vec<Option<CellValuePrimitive>>>>,
933    #[serde(skip_serializing_if = "Option::is_none")]
934    pub types: Option<Vec<Vec<Option<CellValueKind>>>>,
935    #[serde(skip_serializing_if = "Option::is_none")]
936    pub csv: Option<String>,
937    pub total_rows: u32,
938    #[serde(skip_serializing_if = "Option::is_none")]
939    pub next_offset: Option<u32>,
940}
941
942#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
943pub struct ColumnTypeSummary {
944    pub name: String,
945    pub inferred_type: String,
946    pub nulls: u32,
947    pub distinct: u32,
948    pub top_values: Vec<String>,
949    pub min: Option<f64>,
950    pub max: Option<f64>,
951    pub mean: Option<f64>,
952}
953
954#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
955pub struct TableProfileResponse {
956    pub workbook_id: WorkbookId,
957    pub sheet_name: String,
958    pub table_name: Option<String>,
959    pub headers: Vec<String>,
960    pub column_types: Vec<ColumnTypeSummary>,
961    pub row_count: u32,
962    #[serde(skip_serializing_if = "Vec::is_empty")]
963    pub samples: Vec<TableRow>,
964    #[serde(skip_serializing_if = "Vec::is_empty")]
965    pub notes: Vec<String>,
966}
967
968/// Canonical `range-values` response contract.
969///
970/// CLI `--shape canonical` uses a `values: Vec<RangeValuesEntry>` envelope whenever
971/// at least one range entry is emitted. Because CLI output pruning removes empty arrays,
972/// `values` may be omitted when no valid entries remain (for example, fully invalid
973/// or unparseable range inputs).
974///
975/// CLI output keeps this stable top-level shape in both canonical and compact modes.
976#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
977pub struct RangeValuesResponse {
978    pub workbook_id: WorkbookId,
979    pub sheet_name: String,
980    #[serde(skip_serializing_if = "Vec::is_empty")]
981    pub warnings: Vec<Warning>,
982    pub values: Vec<RangeValuesEntry>,
983}
984
985/// Per-range payload for `range-values`.
986///
987/// `range` is the mandatory correlation key in canonical and compact output.
988/// `next_start_row` is an optional continuation cursor when output is truncated.
989#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
990pub struct RangeValuesEntry {
991    pub range: String,
992    #[serde(skip_serializing_if = "Option::is_none")]
993    pub rows: Option<Vec<Vec<Option<CellValue>>>>,
994    /// Formula text matrix aligned to `rows` when `include_formulas=true`.
995    ///
996    /// Each entry is `Some(formula_text)` for formula-driven cells and `None`
997    /// for literal/non-formula cells.
998    #[serde(skip_serializing_if = "Option::is_none")]
999    pub formulas: Option<Vec<Vec<Option<String>>>>,
1000    #[serde(skip_serializing_if = "Option::is_none")]
1001    pub values: Option<Vec<Vec<Option<CellValuePrimitive>>>>,
1002    /// Dense JSON encoding optimized for agent consumption.
1003    #[serde(skip_serializing_if = "Option::is_none")]
1004    pub dense: Option<RangeValuesDensePayload>,
1005    #[serde(skip_serializing_if = "Option::is_none")]
1006    pub csv: Option<String>,
1007    /// Row-keyed JSON array: each element maps column letters to values.
1008    #[serde(skip_serializing_if = "Option::is_none")]
1009    pub rows_keyed: Option<Vec<RangeValuesRowEntry>>,
1010    #[serde(skip_serializing_if = "Option::is_none")]
1011    pub next_start_row: Option<u32>,
1012}
1013
1014/// A single row in the `rows` output format for `range-values`.
1015///
1016/// Maps column letters to cell values, giving agents a direct row-by-row
1017/// mapping without needing to decode dense encoding.
1018#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1019pub struct RangeValuesRowEntry {
1020    /// 1-based row number in the sheet.
1021    pub row: u32,
1022    /// Column-letter-keyed cell values (only non-empty cells included).
1023    pub cells: BTreeMap<String, CellValuePrimitive>,
1024}
1025
1026#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1027pub struct RangeValuesDensePayload {
1028    /// Encoding contract version.
1029    pub encoding: String,
1030    /// Number of columns represented in each dense row.
1031    pub col_count: u32,
1032    /// Value dictionary. Index 0 is always null.
1033    pub dictionary: Vec<Option<CellValuePrimitive>>,
1034    /// Run-length encoded rows using dictionary indexes.
1035    pub row_runs: Vec<Vec<RangeValuesDenseRun>>,
1036    /// Sparse formulas by row/column, included only when requested.
1037    #[serde(skip_serializing_if = "Vec::is_empty")]
1038    pub formulas: Vec<RangeValuesDenseFormula>,
1039}
1040
1041#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1042pub struct RangeValuesDenseRun {
1043    pub value_idx: u32,
1044    pub len: u32,
1045}
1046
1047#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1048pub struct RangeValuesDenseFormula {
1049    /// Zero-based row index within returned rows.
1050    pub row: u32,
1051    /// Zero-based column index within returned rows.
1052    pub col: u32,
1053    pub formula: String,
1054}
1055
1056#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1057pub struct InspectCellsResponse {
1058    pub workbook_id: WorkbookId,
1059    pub sheet_name: String,
1060    /// Legacy single-range echo. For multi-target requests this is a comma-joined list.
1061    pub range: String,
1062    /// Requested A1 targets when more than one was supplied.
1063    #[serde(skip_serializing_if = "Vec::is_empty")]
1064    pub targets: Vec<String>,
1065    pub cells: Vec<CellSnapshot>,
1066    pub truncated: bool,
1067    /// Machine-consumable budget/continuation metadata.
1068    #[serde(skip_serializing_if = "Option::is_none")]
1069    pub budget: Option<ReadBudget>,
1070}
1071
1072#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1073pub struct CloseWorkbookResponse {
1074    pub workbook_id: WorkbookId,
1075    pub message: String,
1076}
1077
1078#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1079pub struct VbaProjectSummaryResponse {
1080    pub workbook_id: WorkbookId,
1081    pub has_vba: bool,
1082    pub code_page: Option<u16>,
1083    pub sys_kind: Option<String>,
1084    pub modules: Vec<VbaModuleDescriptor>,
1085    pub modules_truncated: bool,
1086    pub references: Vec<VbaReferenceDescriptor>,
1087    pub references_truncated: bool,
1088    pub notes: Vec<String>,
1089}
1090
1091#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1092pub struct VbaModuleDescriptor {
1093    pub name: String,
1094    pub stream_name: String,
1095    pub doc_string: String,
1096    pub text_offset: u64,
1097    pub help_context: u32,
1098    pub module_type: String,
1099    pub read_only: bool,
1100    pub private: bool,
1101}
1102
1103#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1104pub struct VbaReferenceDescriptor {
1105    pub kind: String,
1106    pub debug: String,
1107}
1108
1109#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1110pub struct VbaModuleSourceResponse {
1111    pub workbook_id: WorkbookId,
1112    pub module_name: String,
1113    pub offset_lines: u32,
1114    pub limit_lines: u32,
1115    pub total_lines: u32,
1116    pub truncated: bool,
1117    pub source: String,
1118}
1119
1120// ── layout-page ──────────────────────────────────────────────────────────────
1121
1122#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1123#[serde(rename_all = "snake_case")]
1124pub enum LayoutMode {
1125    #[default]
1126    Values,
1127    Formulas,
1128}
1129
1130#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
1131#[serde(rename_all = "snake_case")]
1132pub enum LayoutRender {
1133    #[default]
1134    Json,
1135    Ascii,
1136    Both,
1137}
1138
1139#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1140pub struct LayoutPageColumnInfo {
1141    /// Column letter (e.g., "A")
1142    pub col: String,
1143    /// 1-based column index
1144    pub index: u32,
1145    /// Column width in Excel character units (capped at max_col_width)
1146    pub width_chars: f64,
1147    /// True when no explicit width was set (using the Excel default of 8.43)
1148    #[serde(skip_serializing_if = "std::ops::Not::not")]
1149    pub is_default_width: bool,
1150}
1151
1152#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1153pub struct LayoutCellBorders {
1154    #[serde(skip_serializing_if = "Option::is_none")]
1155    pub top: Option<String>,
1156    #[serde(skip_serializing_if = "Option::is_none")]
1157    pub bottom: Option<String>,
1158    #[serde(skip_serializing_if = "Option::is_none")]
1159    pub left: Option<String>,
1160    #[serde(skip_serializing_if = "Option::is_none")]
1161    pub right: Option<String>,
1162}
1163
1164impl LayoutCellBorders {
1165    pub fn is_empty(&self) -> bool {
1166        self.top.is_none() && self.bottom.is_none() && self.left.is_none() && self.right.is_none()
1167    }
1168}
1169
1170#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1171pub struct LayoutCellInfo {
1172    pub address: String,
1173    #[serde(skip_serializing_if = "Option::is_none")]
1174    pub value: Option<String>,
1175    #[serde(skip_serializing_if = "Option::is_none")]
1176    pub bold: Option<bool>,
1177    #[serde(skip_serializing_if = "Option::is_none")]
1178    pub italic: Option<bool>,
1179    /// Explicit horizontal alignment: "left", "center", "right"
1180    #[serde(skip_serializing_if = "Option::is_none")]
1181    pub align_h: Option<String>,
1182    #[serde(skip_serializing_if = "Option::is_none")]
1183    pub borders: Option<LayoutCellBorders>,
1184    /// True when this cell is the top-left of a merged range
1185    #[serde(skip_serializing_if = "Option::is_none")]
1186    pub merge_start: Option<bool>,
1187}
1188
1189#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1190pub struct LayoutRowInfo {
1191    pub row: u32,
1192    pub cells: Vec<LayoutCellInfo>,
1193}
1194
1195#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1196pub struct LayoutPageResponse {
1197    pub workbook_id: WorkbookId,
1198    pub sheet_name: String,
1199    /// The effective range that was rendered
1200    pub range: String,
1201    pub columns: Vec<LayoutPageColumnInfo>,
1202    /// Merged cell ranges that overlap the rendered region (e.g., ["B1:C1"])
1203    #[serde(skip_serializing_if = "Vec::is_empty")]
1204    pub merged_cells: Vec<String>,
1205    pub rows: Vec<LayoutRowInfo>,
1206    /// ASCII art render (present when render=ascii or render=both)
1207    #[serde(skip_serializing_if = "Option::is_none")]
1208    pub ascii_render: Option<String>,
1209    /// True when the requested range was capped to the row/column limits
1210    #[serde(skip_serializing_if = "std::ops::Not::not")]
1211    pub truncated: bool,
1212    #[serde(skip_serializing_if = "Vec::is_empty")]
1213    pub notes: Vec<String>,
1214}
1215
1216#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1217pub struct GridPayload {
1218    pub sheet: String,
1219    pub anchor: String,
1220    #[serde(default)]
1221    #[serde(skip_serializing_if = "Vec::is_empty")]
1222    pub columns: Vec<GridColumnHint>,
1223    #[serde(default)]
1224    #[serde(skip_serializing_if = "Vec::is_empty")]
1225    pub merges: Vec<String>,
1226    pub rows: Vec<GridRow>,
1227}
1228
1229#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1230pub struct GridColumnHint {
1231    pub offset: u32,
1232    pub width_chars: f64,
1233}
1234
1235#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1236pub struct GridRow {
1237    pub cells: Vec<GridCell>,
1238}
1239
1240#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1241pub struct GridCell {
1242    pub offset: [u32; 2], // [row_offset, col_offset]
1243    #[serde(skip_serializing_if = "Option::is_none")]
1244    pub v: Option<serde_json::Value>,
1245    #[serde(skip_serializing_if = "Option::is_none")]
1246    pub f: Option<String>,
1247    #[serde(skip_serializing_if = "Option::is_none")]
1248    pub fmt: Option<String>,
1249    #[serde(skip_serializing_if = "Option::is_none")]
1250    pub style: Option<crate::model::StylePatch>,
1251}