@nachoggodino/cello 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/BYLAWS.md +446 -0
  2. package/CHANGELOG.md +11 -0
  3. package/LICENSE +12 -0
  4. package/README.md +211 -0
  5. package/dist/cli/cli.d.ts +16 -0
  6. package/dist/cli/cli.js +360 -0
  7. package/dist/cli/serve.d.ts +15 -0
  8. package/dist/cli/serve.js +226 -0
  9. package/dist/evaluator/evaluate.d.ts +2 -0
  10. package/dist/evaluator/evaluate.js +129 -0
  11. package/dist/evaluator/formula.d.ts +13 -0
  12. package/dist/evaluator/formula.js +141 -0
  13. package/dist/formatter/format.d.ts +1 -0
  14. package/dist/formatter/format.js +112 -0
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.js +6 -0
  17. package/dist/parser/parse.d.ts +2 -0
  18. package/dist/parser/parse.js +552 -0
  19. package/dist/renderer/render.d.ts +2 -0
  20. package/dist/renderer/render.js +295 -0
  21. package/dist/serializer/serialize.d.ts +2 -0
  22. package/dist/serializer/serialize.js +104 -0
  23. package/dist/shared/types.d.ts +88 -0
  24. package/dist/shared/types.js +1 -0
  25. package/dist/shared/utils.d.ts +16 -0
  26. package/dist/shared/utils.js +142 -0
  27. package/dist/validator/validate.d.ts +8 -0
  28. package/dist/validator/validate.js +10 -0
  29. package/dist/version.d.ts +1 -0
  30. package/dist/version.js +1 -0
  31. package/docs/ARCHITECTURE.md +43 -0
  32. package/docs/CLI.md +58 -0
  33. package/docs/COMPLIANCE.md +82 -0
  34. package/docs/ERROR_MODEL.md +25 -0
  35. package/docs/FORMULA_SUPPORT.md +33 -0
  36. package/docs/SPEC.md +723 -0
  37. package/docs/SYNTAX_HIGHLIGHTING.md +91 -0
  38. package/examples/advanced_kpi.cel +42 -0
  39. package/examples/basic.cel +8 -0
  40. package/examples/feature_showcase.cel +37 -0
  41. package/package.json +96 -0
  42. package/syntaxes/cel.language-configuration.json +31 -0
  43. package/syntaxes/cel.tmLanguage.json +250 -0
package/docs/SPEC.md ADDED
@@ -0,0 +1,723 @@
1
+ # Cello (.cel) — Specification v1.0
2
+
3
+ > Markdown, but for spreadsheets. Plain-text tabular data with formulas, designed for humans and AI.
4
+
5
+ Cello is a plain-text format for tabular data with formulas. It is human-readable, git-friendly, LLM-friendly, and renders to HTML with multiple sheets, named columns, evaluated formulas, and rich formatting. It is not a replacement for Excel — it is to Excel what Markdown is to Word.
6
+
7
+ The reference implementation is the GPLv3 npm package `@nachoggodino/cello`. Formula evaluation uses HyperFormula under its GPLv3 option.
8
+
9
+ > Implementation note: this document is the target spec. For exact current behavior status, see `docs/COMPLIANCE.md`.
10
+
11
+ ---
12
+
13
+ ## Table of Contents
14
+
15
+ 1. [Philosophy](#1-philosophy)
16
+ 2. [File Structure & Sheets](#2-file-structure--sheets)
17
+ 3. [Sheet Input Formats](#3-sheet-input-formats)
18
+ 4. [Rows](#4-rows)
19
+ 5. [Columns](#5-columns)
20
+ 6. [Column Header Rows](#6-column-header-rows)
21
+ 7. [Row-Level Formatting](#7-row-level-formatting)
22
+ 8. [Data Types](#8-data-types)
23
+ 9. [Formulas](#9-formulas)
24
+ 10. [Merges](#10-merges)
25
+ 11. [Inline Text Formatting](#11-inline-text-formatting)
26
+ 12. [Cell Modifiers](#12-cell-modifiers)
27
+ 13. [Comments](#13-comments)
28
+ 14. [Error Handling](#14-error-handling)
29
+ 15. [Reserved Tokens](#15-reserved-tokens)
30
+ 16. [Full Example](#16-full-example)
31
+ 17. [Implementation Guide](#17-implementation-guide)
32
+ 18. [Use Cases](#18-use-cases)
33
+ 19. [LLM Integration](#19-llm-integration)
34
+ 20. [Out of Scope v1.0](#20-out-of-scope-v10)
35
+
36
+ ---
37
+
38
+ ## 1. Philosophy
39
+
40
+ **The core insight:** LLMs are bad at arithmetic but good at knowing which formulas to use. Cello lets a LLM describe calculations without performing them — the format evaluates them correctly every time.
41
+
42
+ **Design principles:**
43
+
44
+ - **Fail gracefully** — never a total render failure. Unknown syntax degrades to plain text.
45
+ - **Single-pass parseable** — every construct is resolvable line by line with state. No lookahead needed.
46
+ - **LLM-friendly by design** — syntax is minimal, consistent, and easy to generate from a system prompt.
47
+ - **Human-readable** — a `.cel` file opened in any text editor is immediately understandable.
48
+ - **Git-friendly** — plain text, line-oriented diffs, no binary noise.
49
+ - **Format-agnostic data ingestion** — data sheets accept multiple input formats; analysis sheets use Cello syntax.
50
+
51
+ ---
52
+
53
+ ## 2. File Structure & Sheets
54
+
55
+ A `.cel` file contains one or more **sheets**. Each sheet is declared with `@sheet`:
56
+
57
+ ```
58
+ @sheet SheetName [format]
59
+ ```
60
+
61
+ - `SheetName` is case-sensitive.
62
+ - `[format]` is optional — if omitted, the sheet uses native Cello syntax.
63
+ - An optional external source line can be provided immediately after the sheet declaration:
64
+ - `-> /ruta/al/archivo.ext`
65
+ - The declared sheet format still controls parsing.
66
+ - Everything between two `@sheet` declarations belongs to the first.
67
+ - A file with no `@sheet` is treated as a single anonymous sheet in native Cello format.
68
+
69
+ ### Rendering
70
+
71
+ Sheets render as **horizontal tabs** at the top of the HTML output. The first sheet is active by default. If tabs overflow a single line, they scroll horizontally.
72
+
73
+ ---
74
+
75
+ ## 3. Sheet Input Formats
76
+
77
+ The `[format]` modifier on `@sheet` defines how the sheet content is parsed. All formats produce the same internal AST — the rest of the pipeline is identical.
78
+
79
+ ### 3.1 Native Cello (default)
80
+
81
+ No format modifier needed. Full Cello syntax with formulas, modifiers, merges, etc.
82
+
83
+ ```
84
+ @sheet Resumen
85
+
86
+ @header | Métrica | Valor[€][2d] |
87
+ | Total ventas | =SUM(Datos!importe) |
88
+ ```
89
+
90
+ ### 3.2 Delimited formats
91
+
92
+ Any single character can be used as a delimiter:
93
+
94
+ ```
95
+ @sheet Ventas [,] ← comma-separated (CSV)
96
+ @sheet Ventas [,noheader] ← CSV without header row
97
+ @sheet Exports [\t] ← tab-separated (TSV)
98
+ @sheet EuroData [;] ← semicolon-separated (Excel European)
99
+ @sheet Custom [|] ← pipe-separated
100
+ ```
101
+
102
+ Named aliases are available as shorthand:
103
+
104
+ | Alias | Equivalent | Notes |
105
+ |-----------|------------|-------|
106
+ | `[csv]` | `[,]` | RFC 4180 compliant |
107
+ | `[tsv]` | `[\t]` | |
108
+ | `[excel]` | `[;]` | European Excel default |
109
+
110
+ The `noheader` flag is available for all delimited formats:
111
+
112
+ ```
113
+ @sheet Datos [csv:noheader] ← columns assigned letters A, B, C...
114
+ @sheet Datos [\t:noheader]
115
+ ```
116
+
117
+ All delimited formats use a single generic parser with a delimiter parameter. There is no performance difference between them.
118
+
119
+ ### 3.5 External sheet source
120
+
121
+ You can load a sheet from an external file while keeping the same format declaration:
122
+
123
+ ```
124
+ @sheet Ventas [csv]
125
+ -> ./exports/ventas.csv
126
+ ```
127
+
128
+ Rules:
129
+ - `-> path` must appear before any row content in that sheet.
130
+ - Relative paths are resolved from the parser `baseDir` (or process cwd when omitted).
131
+ - If loading fails, parsing continues with a warning diagnostic.
132
+
133
+ ### 3.3 Markdown tables
134
+
135
+ ```
136
+ @sheet Datos [markdown]
137
+
138
+ | nombre | edad | ciudad |
139
+ |--------|------|--------|
140
+ | Ana | 25 | Madrid |
141
+ ```
142
+
143
+ The separator row (`|---|---|`) is ignored. First row is treated as headers. This allows copy-pasting existing Markdown tables directly into a `.cel` file.
144
+
145
+ ### 3.4 JSON
146
+
147
+ ```
148
+ @sheet Datos [json]
149
+
150
+ [
151
+ {"nombre": "Ana", "edad": 25, "ciudad": "Madrid"},
152
+ {"nombre": "Luis", "edad": 32, "ciudad": "Barcelona"}
153
+ ]
154
+ ```
155
+
156
+ - Only flat arrays of objects are supported in v1.0.
157
+ - First object's keys become column headers.
158
+ - For complex nested JSON, flatten externally first and paste the result.
159
+ - JSONPath selection is planned for v1.1: `[json:$.items]`
160
+
161
+ ---
162
+
163
+ ## 4. Rows
164
+
165
+ - Every row is a line containing `|` delimiters.
166
+ - Rows are **auto-numbered** starting at 1, top to bottom, per sheet.
167
+ - A **blank line** (no `|`) does not consume a row number and is not rendered.
168
+ - Leading and trailing `|` are optional but recommended for readability.
169
+ - Multiple consecutive spaces inside a cell **collapse to one** on render — use spaces freely to align columns in plain text.
170
+
171
+ ```
172
+ | Manzanas | 1.20 | 50 | ← renders as "Manzanas", "1.20", "50"
173
+ | Peras | 0.90 | 30 |
174
+
175
+ | TOTAL | ... | ... | ← row 3 (blank line is ignored)
176
+ ```
177
+
178
+ ---
179
+
180
+ ## 5. Columns
181
+
182
+ - Columns are **auto-assigned a letter** (A, B, C... Z, AA, AB...) left to right.
183
+ - Column letters are scoped per sheet and reset at each `@sheet`.
184
+ - Both letter references and named references are always valid simultaneously.
185
+
186
+ ---
187
+
188
+ ## 6. Column Header Rows
189
+
190
+ A line that starts with `@header` defines **column names** for all rows below until the next header row:
191
+
192
+ ```
193
+ @header | Producto | Precio | Cantidad | Total |
194
+ ```
195
+
196
+ - Column names apply from the next data row downward.
197
+ - A second header row redefines names from that point on.
198
+ - Named references in formulas resolve against the active column header.
199
+ - Header rows render as `<th>` elements by default.
200
+ - Modifiers `[]` on a column header **apply to all cells in that column**.
201
+
202
+ ```
203
+ @header | Producto | Precio[€][2d] | Stock[0d][bg:#fff9c4] | Activo |
204
+ ```
205
+
206
+ `[hidden]` is currently parsed into column metadata (`column.hidden`) and can be used by tooling; current renderer does not hide header/cells yet.
207
+
208
+ ```
209
+ @header | Producto | Precio[hidden] | Total |
210
+ ```
211
+
212
+ ---
213
+
214
+ ## 7. Row-Level Formatting
215
+
216
+ Modifier blocks placed **before the first `|`** apply to every cell in that row. Arbitrary text before the first pipe is not part of the public Cello format.
217
+
218
+ ```
219
+ | Manzanas | 1.20 | 50 | =Precio*Cantidad |
220
+ | Peras | 0.90 | 30 | =Precio*Cantidad |
221
+ ```
222
+
223
+ - Row modifiers are parsed and preserved in AST/serialization.
224
+ - Row-name formula references (for example `Sheet!row_name.Column`) are not supported.
225
+ - Rows are referenceable by number.
226
+ - Only modifier blocks should appear before the first pipe.
227
+
228
+ ```
229
+ [bold][bg:#f5f5f5] | TOTAL | < | < | =SUM(Total) |
230
+ ```
231
+
232
+ ---
233
+
234
+ ## 8. Data Types
235
+
236
+ Types are inferred automatically:
237
+
238
+ | Type | Rule | Example |
239
+ |---------|-------------------------------|---------------|
240
+ | Number | Parseable as numeric | `42`, `3.14` |
241
+ | Date | Matches `YYYY-MM-DD` | `2024-01-15` |
242
+ | Boolean | Literal `TRUE` or `FALSE` | `TRUE` |
243
+ | Text | Anything else | `hello`, `—` |
244
+
245
+ Force text type with double quotes:
246
+
247
+ ```
248
+ | "123" | ← text, not number
249
+ | "TRUE" | ← text, not boolean
250
+ ```
251
+
252
+ ---
253
+
254
+ ## 9. Formulas
255
+
256
+ Any cell value starting with `=` is a formula. Evaluation is delegated to HyperFormula, with Cello-side translation for supported named column references.
257
+
258
+ ```
259
+ | =Precio*Cantidad |
260
+ | =SUM(Total) |
261
+ | =B2*C2 |
262
+ | =IF(Margen>0.2,"✓","✗") |
263
+ ```
264
+
265
+ Both **named references** and **coordinate references** are valid and can be mixed freely.
266
+
267
+ ### 9.1 Column range references
268
+
269
+ ```
270
+ =Precio ← current row's Precio cell, current sheet
271
+ =SUM(Precio) ← Precio rows before current formula row, current sheet
272
+ =SUM(Precio[*]) ← full Precio data column, current sheet
273
+ =SUM(Precio[2:5]) ← rows 2–5 of Precio column
274
+ =AVG(Margen) ← Margen rows before current formula row
275
+ ```
276
+
277
+ Same-sheet named column references are context-sensitive:
278
+
279
+ - In scalar context, `Precio` resolves to the current row cell in that column.
280
+ - In aggregate/range context on the same sheet, `Precio` resolves from the first data row up to the row before the formula row. This prevents footer totals like `=SUM(Precio)` from including themselves.
281
+ - `Precio[*]` forces the full data span of the column, including rows below the formula row.
282
+
283
+ ### 9.2 Cross-sheet references
284
+
285
+ Use `!` as the separator:
286
+
287
+ ```
288
+ =SUM(Ventas!Total)
289
+ =SUM(Ventas!Total[*])
290
+ =Ventas!B4
291
+ =COUNTIF(Datos!edad,25)
292
+ ```
293
+
294
+ `!!` is an alias for the first sheet name in the workbook:
295
+
296
+ ```
297
+ =SUM(!!amount)
298
+ ```
299
+
300
+ ### 9.3 Formula evaluation order
301
+
302
+ Dependencies are resolved automatically via topological sort (HyperFormula). Circular references are detected and reported as `#CIRCULAR!`.
303
+
304
+ ---
305
+
306
+ ## 10. Merges
307
+
308
+ ### 10.1 Horizontal merge
309
+
310
+ A cell containing only `<` merges with the previous cell (extends rightward):
311
+
312
+ ```
313
+ | ## Informe Q1 | < | < | valor | ← spans 3 columns
314
+ ```
315
+
316
+ ### 10.2 Vertical merge
317
+
318
+ A cell containing only `^` merges with the cell directly above (extends downward):
319
+
320
+ ```
321
+ | Grupo A | val1 |
322
+ | ^ | val2 | ← "Grupo A" spans 2 rows
323
+ | ^ | val3 | ← spans 3 rows
324
+ | Grupo B | val4 |
325
+ ```
326
+
327
+ ### 10.3 Parser resolution
328
+
329
+ Merge tokens are resolved during the single-pass parse using the previously built AST rows. No lookahead required — `<` looks left in the current row, `^` looks up in the previous row at the same column index.
330
+
331
+ ---
332
+
333
+ ## 11. Inline Text Formatting
334
+
335
+ Markdown-style inline formatting is supported inside cell content:
336
+
337
+ | Syntax | Result |
338
+ |---------------|---------------------|
339
+ | `*texto*` | **bold** |
340
+ | `_texto_` | *italic* |
341
+ | `~~texto~~` | ~~strikethrough~~ |
342
+ | `# texto` | Heading style (`cello-h2`) |
343
+ | `## texto` | Heading style (`cello-h1`) |
344
+
345
+ `#` and `##` apply to the entire cell and cannot be combined with other inline formatting in the same cell.
346
+
347
+ ---
348
+
349
+ ## 12. Cell Modifiers `[]`
350
+
351
+ Modifiers control formatting and rendering. They appear after a value with no space:
352
+
353
+ ```
354
+ celda[bold][bg:red]
355
+ ```
356
+
357
+ **Modifier scope and inheritance:**
358
+
359
+ | Where applied | Scope |
360
+ |---------------|-------|
361
+ | Column header `@header | Col[mod] |` | All cells in that column |
362
+ | Row name `ref[mod]` | All cells in that row |
363
+ | Individual cell `value[mod]` | That cell only |
364
+
365
+ Individual cell modifiers **override** column and row modifiers on conflict. Row modifiers override column modifiers.
366
+
367
+ ### 12.1 Column default formulas
368
+
369
+ | Modifier | Meaning |
370
+ |----------|---------|
371
+ | `@defaults | ... |` | Fill empty cells in each matching column with the formula |
372
+
373
+ `default` is a column-only behavior declared in a non-rendered `@defaults` row below the active header. Header, row, and cell-level default modifiers are ignored. The formula may include the leading `=` or omit it. Explicit row values and formulas always win over the column default.
374
+
375
+ ```
376
+ @header | Qty | Price | Total[€][2d] |
377
+ @defaults | | | =Qty*Price |
378
+ | 2 | 3 | ← Total becomes =Qty*Price and renders as €6.00
379
+ | 4 | 5 | 99 ← explicit Total value is preserved
380
+ ```
381
+
382
+ `@defaults` rows update column metadata only. They do not render and do not consume row numbers.
383
+
384
+ ### 12.2 Numeric format
385
+
386
+ | Modifier | Meaning |
387
+ |----------|---------|
388
+ | `[€]` | Prefix with € symbol |
389
+ | `[$]` | Prefix with $ symbol |
390
+ | `[£]` | Prefix with £ symbol |
391
+ | `[%]` | Percentage format |
392
+ | `[Nd]` | N decimal places (e.g. `[2d]`, `[0d]`) |
393
+
394
+ These modifiers are parsed, preserved in AST, and applied by the renderer to numeric cells. When used on a column header, they apply to every numeric cell in that column. Row and cell modifiers follow the normal precedence rules, so a row or cell can override column decimal/currency choices. Percent display multiplies numeric values by 100 before appending `%`.
395
+
396
+ ### 12.3 Color
397
+
398
+ | Syntax | Meaning |
399
+ |----------------|---------|
400
+ | `[#rrggbb]` | Text color (hex) |
401
+ | `[bg:#rrggbb]` | Background color (hex) |
402
+ | `[colorname]` | Text color (CSS named color) |
403
+ | `[bg:colorname]` | Background color (CSS named color) |
404
+ | `[#bg:#fg]` | Both colors shorthand: background:text |
405
+
406
+ Named colors are standard CSS color names (`red`, `blue`, `green`, `orange`, `gold`, etc.).
407
+
408
+ ### 12.4 Style
409
+
410
+ | Modifier | Meaning |
411
+ |------------|---------|
412
+ | `[bold]` | Bold text |
413
+ | `[italic]` | Italic text |
414
+ | `[hidden]` | Parsed metadata flag (render-time hiding not implemented yet) |
415
+
416
+ ### 12.5 Tone presets
417
+
418
+ | Modifier | Meaning |
419
+ |----------|---------|
420
+ | `[tone:ok]` | Positive/success emphasis |
421
+ | `[tone:warn]` | Warning/caution emphasis |
422
+ | `[tone:error]` | Error/failure emphasis |
423
+ | `[tone:info]` | Informational emphasis |
424
+ | `[tone:muted]` | Secondary/de-emphasized emphasis |
425
+ | `[tone:accent]` | Primary highlight emphasis |
426
+
427
+ Tone presets are rendered as CSS classes (`cello-tone-*`) rather than inline colors so host applications can override them with custom CSS. The built-in renderer defines default foreground/background pairs through CSS variables on `.cello-workbook`.
428
+
429
+ ### 12.6 Combined example
430
+
431
+ ```
432
+ @header | Producto | Precio[€][2d] | Margen[%][1d][bg:#e8f5e9] |
433
+
434
+ [bold][bg:#f0f0f0] | ## TOTAL | < | =SUM(Precio) | =SUM(Margen) |
435
+
436
+ | valor crítico[bg:red][#fff] | < | dato |
437
+ | estado[tone:accent] | normal[tone:ok] | atención[tone:warn] |
438
+ ```
439
+
440
+ ---
441
+
442
+ ## 13. Comments
443
+
444
+ Supported **outside rows only**, using `//`. Not supported inside cell values.
445
+
446
+ ```
447
+ // Annual sales report — generated by agent on 2024-01-15
448
+
449
+ @sheet Datos [csv]
450
+ // Raw data from CRM export
451
+ nombre,edad,importe
452
+ Ana,25,120
453
+ ```
454
+
455
+ ---
456
+
457
+ ## 14. Error Handling
458
+
459
+ Primary non-strict contract: parsing/evaluation tries to continue and diagnostics accumulate on `workbook.diagnostics`.
460
+
461
+ | Stage | Behavior |
462
+ |-------|----------|
463
+ | Parse | Non-row native lines -> warning diagnostics and skipped |
464
+ | Parse (json sheet) | Invalid JSON -> warning + single fallback text row |
465
+ | Evaluate | Missing HyperFormula -> warning, formulas left unresolved |
466
+ | Evaluate | Engine/runtime failure -> error diagnostic; throw only in strict evaluate mode |
467
+ | Formula parse error in engine | `computed` falls back to original formula text |
468
+
469
+ **Strict mode:** `strict: true` propagates parse/evaluate throws; warnings alone do not throw.
470
+
471
+ ```javascript
472
+ render(celContent, { strict: true }) // throws when parse/evaluate throw
473
+ render(celContent) // returns HTML, diagnostics on AST/eval path
474
+ ```
475
+
476
+ ---
477
+
478
+ ## 15. Reserved Tokens
479
+
480
+ | Token | Meaning |
481
+ |------------|---------|
482
+ | `@sheet` | Sheet declaration |
483
+ | `=` | Formula prefix (start of cell value) |
484
+ | `<` | Horizontal merge continuation |
485
+ | `^` | Vertical merge continuation |
486
+ | `@header` | Column header row marker |
487
+ | `//` | Comment (outside rows only) |
488
+ | `"..."` | Force text type |
489
+ | `!` | Cross-sheet reference separator |
490
+ | `->` | External sheet source line |
491
+ | `[n:m]` | Row range in column references |
492
+ | `[...]` | Modifier block |
493
+
494
+ ---
495
+
496
+ ## 16. Full Example
497
+
498
+ ```
499
+ // Q1 Sales Report — generated 2024-01-15
500
+
501
+ @sheet Datos [csv]
502
+ nombre,edad,ciudad,importe
503
+ Ana,25,Madrid,120
504
+ Luis,32,Barcelona,340
505
+ Sara,25,Madrid,89
506
+ Pedro,32,Valencia,210
507
+ Marta,25,Barcelona,95
508
+
509
+ @sheet Por_Edad
510
+
511
+ // KPIs grouped by age
512
+ @header | edad[0d] | total[€][2d] | contador[0d] | ticket_medio[€][2d] |
513
+ | 25 | =SUMIF(Datos!edad,25,Datos!importe) | =COUNTIF(Datos!edad,25) | =total/contador |
514
+ | 32 | =SUMIF(Datos!edad,32,Datos!importe) | =COUNTIF(Datos!edad,32) | =total/contador |
515
+
516
+ [bold][bg:#f0f0f0] | ## TOTAL | =SUM(total) | =SUM(contador) | =SUM(total)/SUM(contador) |
517
+
518
+ @sheet Resumen
519
+
520
+ @header | Métrica | Valor |
521
+ | Total revenue | =SUM(Por_Edad!total) |
522
+ | Total clientes | =SUM(Por_Edad!contador) |
523
+ | Ticket medio | =AVG(Por_Edad!ticket_medio) |
524
+ | Fecha análisis | 2024-01-15 |
525
+ | Generado por | "agente-ventas-v2" |
526
+ ```
527
+
528
+ ---
529
+
530
+ ## 17. Implementation Guide
531
+
532
+ ### 17.1 Library architecture
533
+
534
+ The reference implementation is a TypeScript/JavaScript npm package called `@nachoggodino/cello`. It exposes five core functions:
535
+
536
+ ```typescript
537
+ parse(text: string, options?): AST
538
+ evaluate(ast: AST, options?): Promise<AST>
539
+ validate(text: string, options?): Promise<{ valid: boolean, diagnostics: Diagnostic[] }>
540
+ render(input: string | AST, options?: { strict?, title?, baseDir?, evaluate?, format?: "document" | "fragment" }): Promise<string>
541
+ serialize(ast: AST): string
542
+ ```
543
+
544
+ There are no built-in AST mutation helpers in current public API.
545
+
546
+ ### 17.2 Parser design
547
+
548
+ The parser processes the file in a **single pass**, line by line. It maintains these state variables:
549
+
550
+ ```javascript
551
+ let currentSheet = null
552
+ let currentHeaders = []
553
+ let previousRowByColumn = new Map()
554
+ let jsonBufferBySheet = new Map()
555
+ let consumedDelimitedHeaderBySheet = new Set()
556
+ ```
557
+
558
+ For each line, the parser checks in order:
559
+ 1. Is it a comment (`//`)? → skip
560
+ 2. Is it a `@sheet` declaration? → open new sheet, reset state
561
+ 3. Is it a header row (`@header | ... |`)? → update `currentHeaders`
562
+ 4. Is it a defaults row (`@defaults | ... |`)? → update active column defaults
563
+ 5. Is it a data row (`|`)? → parse as Cello row
564
+ 6. Is it a blank line? → ignore (does not consume row number)
565
+ 7. Otherwise → handle by active sheet format rules (native/delimited/markdown/json)
566
+
567
+ Merge tokens are resolved immediately during row parsing:
568
+ - `<` → extend the previous cell's `colspan` in the current row
569
+ - `^` → extend the cell above's `rowspan` in `lastRow` at the same column index
570
+
571
+ ### 17.3 Formula evaluation
572
+
573
+ HyperFormula is used as the formula engine when the workbook contains formulas. Formula-free workbooks skip engine loading. The package is licensed as GPLv3 and configures HyperFormula with `licenseKey: "gpl-v3"`. The integration flow:
574
+
575
+ ```
576
+ AST (with formula strings)
577
+
578
+ Translate named refs to coordinates
579
+ "=SUM(Precio)" → "=SUM(B2:B9)" // same-sheet footer total excludes current row
580
+ "=SUM(Precio[*])" → "=SUM(B2:B10)"
581
+ "=Ventas!Total" → sheet cross-reference
582
+
583
+ Feed all sheets to HyperFormula
584
+
585
+ HyperFormula resolves dependency graph (topological sort)
586
+
587
+ Retrieve computed values cell by cell
588
+
589
+ AST with computed values alongside original formulas
590
+ ```
591
+
592
+ The name-to-coordinate translation is the only custom logic needed. HyperFormula handles dependency resolution, error propagation, and all function implementations.
593
+
594
+ ### 17.4 Renderer
595
+
596
+ The renderer evaluates by default, unless `evaluate: false` is passed. It walks the selected AST and produces HTML in one of two shapes:
597
+
598
+ ```
599
+ <style> /* inline CSS */ </style>
600
+ <div class="cello-workbook">
601
+ <div class="cello-tabs">
602
+ <button class="tab active">Datos</button>
603
+ <button class="tab">Por_Edad</button>
604
+ <button class="tab">Resumen</button>
605
+ </div>
606
+ <div class="cello-sheet active"> ... </div>
607
+ <div class="cello-sheet hidden"> ... </div>
608
+ <div class="cello-sheet hidden"> ... </div>
609
+ </div>
610
+ <script> /* inline tab switching JS */ </script>
611
+ ```
612
+
613
+ `format: "document"` is the default and returns a self-contained HTML document (`<!doctype html>`) with `html`, `head`, and `body` wrappers. `format: "fragment"` returns only the inline CSS, workbook container, and inline JS so the output can be embedded inside an existing page. No external dependencies are required in either format.
614
+
615
+ Rendered tables include spreadsheet coordinate chrome: a synthetic top row displays column letters (`A`, `B`, `C`...), and a synthetic first column displays semantic row numbers. This chrome is presentation-only; it is not part of the AST or serialized `.cel` text. Header rows use the same row numbering as formulas, so a header at the top of a sheet is row `1` and the first value row below it is row `2`.
616
+
617
+ ### 17.5 Serializer
618
+
619
+ The serializer converts an AST back to valid `.cel` text. This enables round-trip editing: parse → mutate → serialize → parse again.
620
+
621
+ ```
622
+ AST → serializer → .cel text
623
+ ```
624
+
625
+ Rules:
626
+ - Emits normalized Cello row text (`| ... |`) rather than preserving original spacing/alignment
627
+ - Reconstructs header rows from column metadata
628
+ - Outputs row modifiers before data rows where they exist in the AST
629
+ - Preserves modifier order from parsed AST (`modifiers` array order)
630
+
631
+ ### 17.6 Ecosystem components
632
+
633
+ | Component | Description | Priority |
634
+ |-----------|-------------|----------|
635
+ | `@nachoggodino/cello` (npm) | GPLv3 core library: parse, evaluate, validate, render, serialize | v1 |
636
+ | `cello` CLI | CLI tool: `cello render file.cel > out.html`; `cello serve file.cel` for live previews | v1 |
637
+ | `cello-playground` | Web playground: split-view editor + live preview | v1 |
638
+ | `cello-python` | Python port of parser + renderer | v2 |
639
+ | `cello-vscode` | VSCode extension with live preview | v2 |
640
+ | `cello-react` | React component wrapper | v2 |
641
+
642
+ ### 17.7 Format converters
643
+
644
+ Converters transform external formats into `.cel` text. They are separate utilities, not part of the core library.
645
+
646
+ | Converter | Input | Output | Notes |
647
+ |-----------|-------|--------|-------|
648
+ | `fromCSV(csv, sheetName?)` | CSV string | `@sheet [csv]` block | Trivial |
649
+ | `fromXLSX(buffer)` | Excel binary | Multi-sheet `.cel` | Via SheetJS |
650
+ | `fromMarkdown(md)` | Markdown tables | `@sheet [markdown]` block | |
651
+ | `fromJSON(json, path?)` | JSON array | `@sheet [json]` block | Flat arrays only |
652
+ | `toCSV(cel, sheet)` | `.cel` text | CSV string | Per-sheet export |
653
+ | `toXLSX(cel)` | `.cel` text | Excel binary | Via SheetJS |
654
+
655
+ ---
656
+
657
+ ## 18. Use Cases
658
+
659
+ ### 18.1 Primary pattern: Data sheet + KPI sheets
660
+
661
+ The dominant pattern for LLM-generated analysis:
662
+
663
+ ```
664
+ @sheet RawData [csv] ← data, any size, LLM never touches this
665
+ ...
666
+
667
+ @sheet KPIs ← LLM generates this from schema only
668
+ ...formulas referencing RawData...
669
+
670
+ @sheet Summary ← LLM generates executive summary sheet
671
+ ...
672
+ ```
673
+
674
+ The LLM reads only the first N rows to understand the schema, then generates the analysis sheets with formulas. A tool handles the full data conversion.
675
+
676
+ ### 18.2 Use case examples
677
+
678
+ **Sales analysis**
679
+ ```
680
+ "Here's our monthly sales CSV. Group by region, show total and average ticket."
681
+ → LLM generates KPI sheet with SUMIF/COUNTIF/AVGIF per region
682
+ → Cello evaluates correctly
683
+ → Human reads clean tabular render
684
+ ```
685
+
686
+ **Budget vs actuals**
687
+ ```
688
+ "Here are my actual expenses and my budget plan."
689
+ → Sheet 1: actuals [csv], Sheet 2: budget [csv]
690
+ → LLM generates Sheet 3 with variance formulas and color coding
691
+ → Over-budget cells highlighted in red automatically
692
+ ```
693
+
694
+ **Financial due diligence**
695
+ ```
696
+ "Here are 3 years of P&L data."
697
+ → One sheet per year [csv]
698
+ → LLM generates ratios sheet: CAGR, margins, YoY growth
699
+ → All calculated by Cello, not inferred by LLM
700
+ ```
701
+
702
+ **Data reconciliation**
703
+ ```
704
+ "These two CSVs should match. Find discrepancies."
705
+ → Sheet 1 and Sheet 2 with source data
706
+ → LLM generates Sheet 3 with VLOOKUP cross-sheet comparisons
707
+ → Mismatches surface automatically
708
+ ```
709
+
710
+ **Projection modeling**
711
+ ```
712
+ "If I grow 10% monthly from €1000, show me 12 months."
713
+ → LLM generates single sheet with growth formula per row
714
+ → No arithmetic by the LLM — just the formula pattern
715
+ ```
716
+
717
+ **LLM reading back evaluated results**
718
+ ```
719
+ // Two-step agentic flow:
720
+ Step 1: LLM generates .cel with formulas
721
+ Step 2: cello_evaluate() resolves all formulas to values
722
+ Step 3: cello_to_markdown_table() converts result sheet to plain text
723
+ Step 4: LLM reads plain numbers and generates written ana