f8-engine 0.1.0__tar.gz → 0.3.0__tar.gz

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 (103) hide show
  1. {f8_engine-0.1.0 → f8_engine-0.3.0}/Cargo.lock +2 -2
  2. {f8_engine-0.1.0 → f8_engine-0.3.0}/Cargo.toml +1 -1
  3. {f8_engine-0.1.0 → f8_engine-0.3.0}/PKG-INFO +44 -40
  4. {f8_engine-0.1.0 → f8_engine-0.3.0}/README.md +43 -39
  5. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/compiled/mod.rs +12 -2
  6. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/compiled/model.rs +10 -2
  7. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/emission/format.rs +45 -11
  8. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/emission/workbook.rs +43 -47
  9. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/model.rs +1 -0
  10. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/resolve.rs +199 -181
  11. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/cell.rs +11 -1
  12. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/error.rs +12 -1
  13. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/expand.rs +47 -2
  14. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/symbol.rs +1 -0
  15. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/layout/error.rs +19 -0
  16. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/layout/mod.rs +267 -71
  17. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/lowering/address.rs +39 -7
  18. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/lowering/mod.rs +30 -34
  19. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/expr/ast.rs +170 -14
  20. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/expr/mod.rs +2 -1
  21. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/expr/parser.rs +114 -187
  22. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/lib.rs +1 -0
  23. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/error.rs +3 -0
  24. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/format.rs +33 -0
  25. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/mod.rs +4 -3
  26. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/table.rs +227 -8
  27. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/theme.rs +27 -10
  28. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-python/src/error.rs +14 -2
  29. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-python/src/lib.rs +8 -0
  30. {f8_engine-0.1.0 → f8_engine-0.3.0}/pyproject.toml +4 -0
  31. {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/__init__.py +4 -1
  32. {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/_core.pyi +3 -0
  33. {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/audit.py +10 -2
  34. {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/builder.py +253 -10
  35. {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/cli.py +54 -4
  36. f8_engine-0.3.0/python/f8/skills/f8-modeling/SKILL.md +45 -0
  37. f8_engine-0.3.0/python/f8/skills/f8-modeling/references/auditing-and-repair.md +170 -0
  38. f8_engine-0.3.0/python/f8/skills/f8-modeling/references/functions.md +128 -0
  39. f8_engine-0.3.0/python/f8/skills/f8-modeling/references/syntax-and-grid.md +324 -0
  40. {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/types.py +49 -6
  41. f8_engine-0.1.0/python/f8/skills/f8-modeling/SKILL.md +0 -43
  42. f8_engine-0.1.0/python/f8/skills/f8-modeling/references/auditing-and-repair.md +0 -366
  43. f8_engine-0.1.0/python/f8/skills/f8-modeling/references/functions.md +0 -172
  44. f8_engine-0.1.0/python/f8/skills/f8-modeling/references/syntax-and-grid.md +0 -342
  45. {f8_engine-0.1.0 → f8_engine-0.3.0}/LICENSE +0 -0
  46. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/Cargo.toml +0 -0
  47. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/compiled/error.rs +0 -0
  48. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/emission/error.rs +0 -0
  49. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/emission/mod.rs +0 -0
  50. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/error.rs +0 -0
  51. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/context.rs +0 -0
  52. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/error.rs +0 -0
  53. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/expr.rs +0 -0
  54. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/eval/mod.rs +0 -0
  55. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/cycle.rs +0 -0
  56. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/dependency.rs +0 -0
  57. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/error.rs +0 -0
  58. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/graph/mod.rs +0 -0
  59. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/id.rs +0 -0
  60. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/ir/mod.rs +0 -0
  61. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/layout/a1.rs +0 -0
  62. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/lowering/error.rs +0 -0
  63. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/compiler/mod.rs +0 -0
  64. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/expr/error.rs +0 -0
  65. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/common.rs +0 -0
  66. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/constructors.rs +0 -0
  67. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/mod.rs +0 -0
  68. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/parts.rs +0 -0
  69. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/serial.rs +0 -0
  70. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/date/yearfrac.rs +0 -0
  71. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/error.rs +0 -0
  72. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/financial/annuity.rs +0 -0
  73. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/financial/discounting.rs +0 -0
  74. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/financial/irr.rs +0 -0
  75. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/financial/mod.rs +0 -0
  76. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/logical/boolean.rs +0 -0
  77. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/logical/branching.rs +0 -0
  78. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/logical/error_handling.rs +0 -0
  79. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/logical/mod.rs +0 -0
  80. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/index.rs +0 -0
  81. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/mod.rs +0 -0
  82. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/search.rs +0 -0
  83. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/xlookup.rs +0 -0
  84. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/lookup/xmatch.rs +0 -0
  85. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/aggregation.rs +0 -0
  86. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/basic.rs +0 -0
  87. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/conditional.rs +0 -0
  88. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/criteria.rs +0 -0
  89. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/mod.rs +0 -0
  90. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/product.rs +0 -0
  91. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/math/rounding.rs +0 -0
  92. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/mod.rs +0 -0
  93. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/registry.rs +0 -0
  94. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/functions/traits.rs +0 -0
  95. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/id.rs +0 -0
  96. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/model/manifest.rs +0 -0
  97. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/value/error.rs +0 -0
  98. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/value/format.rs +0 -0
  99. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-core/src/value/mod.rs +0 -0
  100. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-python/Cargo.toml +0 -0
  101. {f8_engine-0.1.0 → f8_engine-0.3.0}/crates/f8-python/src/model.rs +0 -0
  102. {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/__main__.py +0 -0
  103. {f8_engine-0.1.0 → f8_engine-0.3.0}/python/f8/py.typed +0 -0
@@ -37,7 +37,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
37
37
 
38
38
  [[package]]
39
39
  name = "f8-core"
40
- version = "0.1.0"
40
+ version = "0.3.0"
41
41
  dependencies = [
42
42
  "indexmap",
43
43
  "petgraph",
@@ -50,7 +50,7 @@ dependencies = [
50
50
 
51
51
  [[package]]
52
52
  name = "f8-python"
53
- version = "0.1.0"
53
+ version = "0.3.0"
54
54
  dependencies = [
55
55
  "f8-core",
56
56
  "pyo3",
@@ -3,7 +3,7 @@ resolver = "3"
3
3
  members = ["crates/f8-core", "crates/f8-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.1.0"
6
+ version = "0.3.0"
7
7
  edition = "2024"
8
8
  description = "Financial modeling engine with compiler verification and live Excel formulas"
9
9
  authors = ["Michael (Chencheng) Hu, CFA"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: f8-engine
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: Intended Audience :: Financial and Insurance Industry
@@ -26,27 +26,27 @@ Project-URL: Repository, https://github.com/mhcfa/f8
26
26
 
27
27
  # f8
28
28
 
29
- Financial modeling engine with compiler verification and live Excel formulas.
29
+ Author, verify, and automate financial models with code and AI agents, compiled into live Excel workbooks.
30
30
 
31
- ## Why f8
31
+ ## Who is f8 for
32
32
 
33
- I am a CFA charterholder working primarily on private company M&A and project finance, domains that live in complex, bespoke Excel financial models.
33
+ 1. **People who want AI to reliably build complex Excel models (financial and beyond)**: Give AI agents a semantic 2D DSL and compile-time verification to produce clean and error-free `.xlsx` workbooks with live formulas.
34
+ 2. **People doing financial analysis in pure code (no Excel required)**: Run row-by-row financial statements, corkscrew schedules, and cross-table dependency graphs in Python (where columnar DataFrames fall short), with `compiled.save("model.xlsx")` ready whenever a stakeholder asks for the spreadsheet.
34
35
 
35
- Spreadsheets have always suffered from fragile manual work, silent copy-paste errors, and no version control.
36
+ ## Why f8
36
37
 
37
- When AI emerged, I found that AI agents struggle to manipulate Excel effectively (whether via OpenXML, openpyxl, Office.js, or GUI), primarily because of:
38
+ I am a CFA charterholder working in private M&A and project finance, where complex Excel models remain the non-negotiable deliverable despite fragile manual work, silent copy-paste errors, and zero version control.
38
39
 
39
- - Brittle coordinates: Spreadsheets force agents to reason in spatial coordinates rather than semantics; inserting a single row shifts every downstream reference.
40
- - No verification loop: Standard spreadsheet libraries do not evaluate formulas, so an agent has to inspect the `.xlsx` file just to get feedback.
40
+ AI agents struggle to manipulate raw spreadsheets (`openpyxl`, Office.js, or GUI) for two reasons:
41
41
 
42
- Yet the industry is stubborn: Excel remains the non-negotiable deliverable.
42
+ - Brittle coordinates: Agents must reason in spatial cell addresses (`C42:G42`) instead of domain semantics; inserting one row shifts every downstream reference.
43
+ - No verification loop: Spreadsheet libraries do not evaluate formulas, leaving agents blind until the `.xlsx` file is opened and inspected.
43
44
 
44
- `f8` brings software engineering rigor and AI-native ergonomics to traditional financial modeling. The `.xlsx` workbook becomes a compiled artifact rather than a hand-edited canvas.
45
+ `f8` turns the `.xlsx` workbook into a compiled artifact rather than a hand-edited canvas:
45
46
 
46
- - Semantic 2D modeling: Author models in code using readable coordinates; `f8` handles spatial layout and lowers logic into native Excel formulas.
47
- - Compiler-enforced rigor: A 6-pass Rust compiler verifies grid completeness (zero unassigned cells or ambiguous formula overlaps) and enforces a strict DAG via Tarjan cycle detection before a file is ever written.
48
- - Closed-loop AI verification: `f8` evaluates formulas in memory with 100% Excel parity (yes, even the 1900 leap day bug in Excel) so AI agents can audit calculated tables, trace precedent/dependent trees, verify accounting checks, and self-heal errors in milliseconds. This also means the model is directly testable in Python.
49
- - End-to-end fundamental workflow in code: Keep data ingestion, domain modeling, automated auditing, and live `.xlsx` emission in version-controlled code with full AI agent assistance.
47
+ - Semantic 2D modeling: Author models with readable table, row, and column identifiers; `f8` handles spatial layout and lowers logic into native Excel formulas.
48
+ - Compiler-enforced rigor: A 6-pass Rust compiler verifies grid completeness (zero unassigned cells or formula overlaps) and enforces a strict DAG via Tarjan cycle detection.
49
+ - Closed-loop verification: Evaluates formulas in memory with 100% Excel parity (including the 1900 leap day bug) so AI agents and `pytest` suites can audit tables, trace dependencies, and verify accounting checks in milliseconds.
50
50
 
51
51
  ## Getting started
52
52
 
@@ -62,6 +62,7 @@ If you are working with an AI coding agent, `f8` ships with a bundled modeling s
62
62
 
63
63
  ```bash
64
64
  f8 agent init # scaffolds .agents/skills/f8-modeling/
65
+ f8 theme init # generates a customizable theme.json in the workspace root
65
66
  f8 guide --all # streams the full DSL, function, and auditing guide to stdout
66
67
  ```
67
68
 
@@ -70,48 +71,50 @@ f8 guide --all # streams the full DSL, function, and auditing guide to stdout
70
71
  ```python
71
72
  from f8 import Model
72
73
 
74
+ years = ["2025A", "2026E", "2027E"]
75
+ capex = {"2025A": 25.0, "2026E": 30.0, "2027E": 35.0}
76
+ depr = {"2025A": -15.0, "2026E": -18.0, "2027E": -22.0}
77
+
73
78
  model = Model("Operating Forecast")
74
79
  sheet = model.add_sheet("Summary")
75
80
 
76
- pnl = sheet.add_table("PnL", "Three-Year Projection", "($ in millions)")
77
- pnl.add_column("item", role="descriptor", header="Line Item")
78
- for year in ["2025A", "2026E", "2027E"]:
79
- pnl.add_column(year, header=year, format="currency", decimals=1)
81
+ ppe = sheet.add_table("PPE", "PP&E Corkscrew Schedule", "($ in millions)")
82
+ ppe.add_column("item", role="descriptor", header="Line Item")
83
+ for year in years:
84
+ ppe.add_column(year, header=year, format="currency", decimals=1)
80
85
 
81
- pnl.add_row(
82
- "revenue",
83
- cells={"item": "Revenue", "2025A": 100.0},
84
- expr='PnL["revenue", col.prev] * 1.25',
85
- )
86
- pnl.add_row(
87
- "cogs",
88
- cells={"item": "Cost of Revenue"},
89
- expr='PnL["revenue", @col] * 0.30',
86
+ ppe.add_row(
87
+ "beg_balance",
88
+ cells={"item": "Beginning Balance", "2025A": 100.0},
89
+ expr='PPE["end_balance", col.prev]',
90
90
  )
91
- pnl.add_row(
92
- "gross_profit",
93
- cells={"item": "Gross Profit"},
94
- expr='PnL["revenue", @col] - PnL["cogs", @col]',
91
+ ppe.add_row("capex", cells={"item": "Capital Expenditures", **capex})
92
+ ppe.add_row("depreciation", cells={"item": "Depreciation", **depr})
93
+ ppe.add_row(
94
+ "end_balance",
95
+ cells={"item": "Ending Balance"},
96
+ expr='SUM(PPE["beg_balance".."depreciation", @col])',
95
97
  style="total",
96
98
  )
97
99
 
98
100
  compiled = model.compile()
99
101
 
100
102
  # Audit evaluated values in memory (or pass view="formulas" to inspect lowered Excel formulas)
101
- print(compiled.table("PnL").to_markdown())
103
+ print(compiled.table("PPE").to_markdown())
102
104
 
103
- # Emit a live .xlsx workbook with native Excel formulas and styling
105
+ # Emit a live .xlsx workbook with native Excel formulas and styling (you don't have to)
104
106
  compiled.save("forecast.xlsx")
105
107
  ```
106
108
 
107
109
  Output:
108
110
 
109
111
  ```markdown
110
- | Line Item | 2025A | 2026E | 2027E |
111
- | :--------------- | --------: | --------: | ---------: |
112
- | Revenue | $100.0 | $125.0 | $156.2 |
113
- | Cost of Revenue | $30.0 | $37.5 | $46.9 |
114
- | **Gross Profit** | **$70.0** | **$87.5** | **$109.4** |
112
+ | Line Item | 2025A | 2026E | 2027E |
113
+ | :------------------- | ---------: | ---------: | ---------: |
114
+ | Beginning Balance | $100.0 | $110.0 | $122.0 |
115
+ | Capital Expenditures | $25.0 | $30.0 | $35.0 |
116
+ | Depreciation | ($15.0) | ($18.0) | ($22.0) |
117
+ | **Ending Balance** | **$110.0** | **$122.0** | **$135.0** |
115
118
  ```
116
119
 
117
120
  ## Sample deliverables
@@ -150,9 +153,10 @@ Under the hood, `f8` is a Python SDK (`python/f8/`) backed by a 6-pass Rust comp
150
153
  `Spec -> IR Expansion -> DAG & Cycle Check -> Topological Evaluation -> Spatial Layout -> Formula Lowering -> .xlsx Emission`
151
154
 
152
155
  - Architecture overview: `docs/architecture.md`
153
- - 2D matrix DSL reference: `docs/dsl/spec-format.md`
154
- - Standard function catalog: `docs/functions/catalog.md`
155
156
  - Python modeling skill: `python/f8/skills/f8-modeling/SKILL.md`
157
+ - Syntax, grid, and formatting reference: `python/f8/skills/f8-modeling/references/syntax-and-grid.md`
158
+ - Function reference: `python/f8/skills/f8-modeling/references/functions.md`
159
+ - Auditing and repair reference: `python/f8/skills/f8-modeling/references/auditing-and-repair.md`
156
160
 
157
161
  ## License
158
162
 
@@ -1,26 +1,26 @@
1
1
  # f8
2
2
 
3
- Financial modeling engine with compiler verification and live Excel formulas.
3
+ Author, verify, and automate financial models with code and AI agents, compiled into live Excel workbooks.
4
4
 
5
- ## Why f8
5
+ ## Who is f8 for
6
6
 
7
- I am a CFA charterholder working primarily on private company M&A and project finance, domains that live in complex, bespoke Excel financial models.
7
+ 1. **People who want AI to reliably build complex Excel models (financial and beyond)**: Give AI agents a semantic 2D DSL and compile-time verification to produce clean and error-free `.xlsx` workbooks with live formulas.
8
+ 2. **People doing financial analysis in pure code (no Excel required)**: Run row-by-row financial statements, corkscrew schedules, and cross-table dependency graphs in Python (where columnar DataFrames fall short), with `compiled.save("model.xlsx")` ready whenever a stakeholder asks for the spreadsheet.
8
9
 
9
- Spreadsheets have always suffered from fragile manual work, silent copy-paste errors, and no version control.
10
+ ## Why f8
10
11
 
11
- When AI emerged, I found that AI agents struggle to manipulate Excel effectively (whether via OpenXML, openpyxl, Office.js, or GUI), primarily because of:
12
+ I am a CFA charterholder working in private M&A and project finance, where complex Excel models remain the non-negotiable deliverable despite fragile manual work, silent copy-paste errors, and zero version control.
12
13
 
13
- - Brittle coordinates: Spreadsheets force agents to reason in spatial coordinates rather than semantics; inserting a single row shifts every downstream reference.
14
- - No verification loop: Standard spreadsheet libraries do not evaluate formulas, so an agent has to inspect the `.xlsx` file just to get feedback.
14
+ AI agents struggle to manipulate raw spreadsheets (`openpyxl`, Office.js, or GUI) for two reasons:
15
15
 
16
- Yet the industry is stubborn: Excel remains the non-negotiable deliverable.
16
+ - Brittle coordinates: Agents must reason in spatial cell addresses (`C42:G42`) instead of domain semantics; inserting one row shifts every downstream reference.
17
+ - No verification loop: Spreadsheet libraries do not evaluate formulas, leaving agents blind until the `.xlsx` file is opened and inspected.
17
18
 
18
- `f8` brings software engineering rigor and AI-native ergonomics to traditional financial modeling. The `.xlsx` workbook becomes a compiled artifact rather than a hand-edited canvas.
19
+ `f8` turns the `.xlsx` workbook into a compiled artifact rather than a hand-edited canvas:
19
20
 
20
- - Semantic 2D modeling: Author models in code using readable coordinates; `f8` handles spatial layout and lowers logic into native Excel formulas.
21
- - Compiler-enforced rigor: A 6-pass Rust compiler verifies grid completeness (zero unassigned cells or ambiguous formula overlaps) and enforces a strict DAG via Tarjan cycle detection before a file is ever written.
22
- - Closed-loop AI verification: `f8` evaluates formulas in memory with 100% Excel parity (yes, even the 1900 leap day bug in Excel) so AI agents can audit calculated tables, trace precedent/dependent trees, verify accounting checks, and self-heal errors in milliseconds. This also means the model is directly testable in Python.
23
- - End-to-end fundamental workflow in code: Keep data ingestion, domain modeling, automated auditing, and live `.xlsx` emission in version-controlled code with full AI agent assistance.
21
+ - Semantic 2D modeling: Author models with readable table, row, and column identifiers; `f8` handles spatial layout and lowers logic into native Excel formulas.
22
+ - Compiler-enforced rigor: A 6-pass Rust compiler verifies grid completeness (zero unassigned cells or formula overlaps) and enforces a strict DAG via Tarjan cycle detection.
23
+ - Closed-loop verification: Evaluates formulas in memory with 100% Excel parity (including the 1900 leap day bug) so AI agents and `pytest` suites can audit tables, trace dependencies, and verify accounting checks in milliseconds.
24
24
 
25
25
  ## Getting started
26
26
 
@@ -36,6 +36,7 @@ If you are working with an AI coding agent, `f8` ships with a bundled modeling s
36
36
 
37
37
  ```bash
38
38
  f8 agent init # scaffolds .agents/skills/f8-modeling/
39
+ f8 theme init # generates a customizable theme.json in the workspace root
39
40
  f8 guide --all # streams the full DSL, function, and auditing guide to stdout
40
41
  ```
41
42
 
@@ -44,48 +45,50 @@ f8 guide --all # streams the full DSL, function, and auditing guide to stdout
44
45
  ```python
45
46
  from f8 import Model
46
47
 
48
+ years = ["2025A", "2026E", "2027E"]
49
+ capex = {"2025A": 25.0, "2026E": 30.0, "2027E": 35.0}
50
+ depr = {"2025A": -15.0, "2026E": -18.0, "2027E": -22.0}
51
+
47
52
  model = Model("Operating Forecast")
48
53
  sheet = model.add_sheet("Summary")
49
54
 
50
- pnl = sheet.add_table("PnL", "Three-Year Projection", "($ in millions)")
51
- pnl.add_column("item", role="descriptor", header="Line Item")
52
- for year in ["2025A", "2026E", "2027E"]:
53
- pnl.add_column(year, header=year, format="currency", decimals=1)
55
+ ppe = sheet.add_table("PPE", "PP&E Corkscrew Schedule", "($ in millions)")
56
+ ppe.add_column("item", role="descriptor", header="Line Item")
57
+ for year in years:
58
+ ppe.add_column(year, header=year, format="currency", decimals=1)
54
59
 
55
- pnl.add_row(
56
- "revenue",
57
- cells={"item": "Revenue", "2025A": 100.0},
58
- expr='PnL["revenue", col.prev] * 1.25',
59
- )
60
- pnl.add_row(
61
- "cogs",
62
- cells={"item": "Cost of Revenue"},
63
- expr='PnL["revenue", @col] * 0.30',
60
+ ppe.add_row(
61
+ "beg_balance",
62
+ cells={"item": "Beginning Balance", "2025A": 100.0},
63
+ expr='PPE["end_balance", col.prev]',
64
64
  )
65
- pnl.add_row(
66
- "gross_profit",
67
- cells={"item": "Gross Profit"},
68
- expr='PnL["revenue", @col] - PnL["cogs", @col]',
65
+ ppe.add_row("capex", cells={"item": "Capital Expenditures", **capex})
66
+ ppe.add_row("depreciation", cells={"item": "Depreciation", **depr})
67
+ ppe.add_row(
68
+ "end_balance",
69
+ cells={"item": "Ending Balance"},
70
+ expr='SUM(PPE["beg_balance".."depreciation", @col])',
69
71
  style="total",
70
72
  )
71
73
 
72
74
  compiled = model.compile()
73
75
 
74
76
  # Audit evaluated values in memory (or pass view="formulas" to inspect lowered Excel formulas)
75
- print(compiled.table("PnL").to_markdown())
77
+ print(compiled.table("PPE").to_markdown())
76
78
 
77
- # Emit a live .xlsx workbook with native Excel formulas and styling
79
+ # Emit a live .xlsx workbook with native Excel formulas and styling (you don't have to)
78
80
  compiled.save("forecast.xlsx")
79
81
  ```
80
82
 
81
83
  Output:
82
84
 
83
85
  ```markdown
84
- | Line Item | 2025A | 2026E | 2027E |
85
- | :--------------- | --------: | --------: | ---------: |
86
- | Revenue | $100.0 | $125.0 | $156.2 |
87
- | Cost of Revenue | $30.0 | $37.5 | $46.9 |
88
- | **Gross Profit** | **$70.0** | **$87.5** | **$109.4** |
86
+ | Line Item | 2025A | 2026E | 2027E |
87
+ | :------------------- | ---------: | ---------: | ---------: |
88
+ | Beginning Balance | $100.0 | $110.0 | $122.0 |
89
+ | Capital Expenditures | $25.0 | $30.0 | $35.0 |
90
+ | Depreciation | ($15.0) | ($18.0) | ($22.0) |
91
+ | **Ending Balance** | **$110.0** | **$122.0** | **$135.0** |
89
92
  ```
90
93
 
91
94
  ## Sample deliverables
@@ -124,9 +127,10 @@ Under the hood, `f8` is a Python SDK (`python/f8/`) backed by a 6-pass Rust comp
124
127
  `Spec -> IR Expansion -> DAG & Cycle Check -> Topological Evaluation -> Spatial Layout -> Formula Lowering -> .xlsx Emission`
125
128
 
126
129
  - Architecture overview: `docs/architecture.md`
127
- - 2D matrix DSL reference: `docs/dsl/spec-format.md`
128
- - Standard function catalog: `docs/functions/catalog.md`
129
130
  - Python modeling skill: `python/f8/skills/f8-modeling/SKILL.md`
131
+ - Syntax, grid, and formatting reference: `python/f8/skills/f8-modeling/references/syntax-and-grid.md`
132
+ - Function reference: `python/f8/skills/f8-modeling/references/functions.md`
133
+ - Auditing and repair reference: `python/f8/skills/f8-modeling/references/auditing-and-repair.md`
130
134
 
131
135
  ## License
132
136
 
@@ -277,6 +277,7 @@ impl CompiledModel {
277
277
  decimals: cell_node.decimals,
278
278
  currency: cell_node.currency,
279
279
  is_highlight: cell_node.is_highlight,
280
+ shade: cell_node.shade,
280
281
  indent: cell_node.indent,
281
282
  is_formula,
282
283
  formula_expr,
@@ -309,17 +310,21 @@ impl CompiledModel {
309
310
  .map(|col| match col {
310
311
  crate::model::table::ColumnSpec::Data(d) => ColumnInspection {
311
312
  id: Some(d.id.clone()),
313
+ group: d.group.clone(),
312
314
  headers: d.headers.clone(),
313
315
  is_gutter: false,
314
316
  is_descriptor: d.role.is_descriptor(),
315
317
  style: Some(d.style.unwrap_or(SemanticStyle::Data)),
318
+ shade: d.shade.filter(|s| !s.is_none()),
316
319
  },
317
320
  crate::model::table::ColumnSpec::Gutter => ColumnInspection {
318
321
  id: None,
322
+ group: None,
319
323
  headers: Vec::new(),
320
324
  is_gutter: true,
321
325
  is_descriptor: false,
322
326
  style: None,
327
+ shade: None,
323
328
  },
324
329
  })
325
330
  .collect();
@@ -346,6 +351,7 @@ impl CompiledModel {
346
351
  is_gutter: false,
347
352
  indent,
348
353
  style: Some(d.style.unwrap_or(SemanticStyle::Data)),
354
+ shade: d.shade.filter(|s| !s.is_none()),
349
355
  }
350
356
  }
351
357
  crate::model::table::RowSpec::SectionHeader(s) => RowInspection {
@@ -355,6 +361,7 @@ impl CompiledModel {
355
361
  is_gutter: false,
356
362
  indent: s.indent.unwrap_or(0),
357
363
  style: None,
364
+ shade: None,
358
365
  },
359
366
  crate::model::table::RowSpec::Gutter => RowInspection {
360
367
  id: None,
@@ -363,6 +370,7 @@ impl CompiledModel {
363
370
  is_gutter: true,
364
371
  indent: 0,
365
372
  style: None,
373
+ shade: None,
366
374
  },
367
375
  })
368
376
  .collect();
@@ -641,8 +649,10 @@ impl CompiledModel {
641
649
  }
642
650
  ColumnSpec::Data(d) => {
643
651
  let k_len = d.headers.len();
644
- let mut header_text = if t >= total_tiers - k_len {
645
- let k = t - (total_tiers - k_len);
652
+ let first_hdr_tier = total_tiers.saturating_sub(k_len);
653
+ let mut header_text = if t == 0 && d.group.is_some() {
654
+ d.group.clone()
655
+ } else if let Some(k) = t.checked_sub(first_hdr_tier) {
646
656
  d.headers.get(k).cloned()
647
657
  } else {
648
658
  None
@@ -4,7 +4,7 @@ use indexmap::IndexMap;
4
4
  use serde::{Deserialize, Serialize};
5
5
 
6
6
  use crate::compiler::ir::id::CellId;
7
- use crate::model::format::{CurrencyCode, FormatPreset, SemanticStyle};
7
+ use crate::model::format::{CurrencyCode, FormatPreset, SemanticStyle, Shade};
8
8
  use crate::model::id::{ColId, RowId, SheetId, TableId};
9
9
  use crate::value::CellValue;
10
10
 
@@ -81,6 +81,8 @@ pub struct CellInspection {
81
81
  pub currency: Option<CurrencyCode>,
82
82
  /// Visual highlight emphasis flag.
83
83
  pub is_highlight: bool,
84
+ /// Semantic background shade if assigned.
85
+ pub shade: Option<Shade>,
84
86
  /// Descriptor text indentation level.
85
87
  pub indent: u8,
86
88
  /// `true` if this cell is calculated by formula, `false` if constant literal.
@@ -177,7 +179,9 @@ where
177
179
  pub struct ColumnInspection {
178
180
  /// Column identifier if this is a data column.
179
181
  pub id: Option<ColId>,
180
- /// Multi-tier header labels.
182
+ /// Horizontal spanning group banner label if declared.
183
+ pub group: Option<String>,
184
+ /// Per-column stacked header labels.
181
185
  pub headers: Vec<String>,
182
186
  /// `true` if this column is a blank spacing gutter.
183
187
  pub is_gutter: bool,
@@ -185,6 +189,8 @@ pub struct ColumnInspection {
185
189
  pub is_descriptor: bool,
186
190
  /// Structural presentation style if this is a data column.
187
191
  pub style: Option<SemanticStyle>,
192
+ /// Semantic background shade if this is a data column.
193
+ pub shade: Option<Shade>,
188
194
  }
189
195
 
190
196
  /// Row inspection metadata.
@@ -202,6 +208,8 @@ pub struct RowInspection {
202
208
  pub indent: u8,
203
209
  /// Structural presentation style if this is a calculable data row.
204
210
  pub style: Option<SemanticStyle>,
211
+ /// Semantic background shade if this is a calculable data row.
212
+ pub shade: Option<Shade>,
205
213
  }
206
214
 
207
215
  /// Cell-level audit information within a table inspection view.
@@ -5,7 +5,7 @@ use rust_xlsxwriter::{Color, Format, FormatAlign, FormatBorder, FormatUnderline}
5
5
 
6
6
  use super::error::EmissionError;
7
7
  use crate::compiler::layout::StyleRole;
8
- use crate::model::format::{CurrencyCode, FormatPreset};
8
+ use crate::model::format::{CurrencyCode, FormatPreset, Shade};
9
9
  use crate::model::theme::{BorderStyle, PaletteConfig, Theme};
10
10
 
11
11
  /// Parses a hexadecimal color string (`#RGB`, `#RRGGBB`, or `#RRGGBBAA`) into a `rust_xlsxwriter::Color`.
@@ -94,7 +94,9 @@ struct ParsedPalette {
94
94
  total_text: Color,
95
95
  input_text: Color,
96
96
  formula_text: Color,
97
- zebra_fill: Option<Color>,
97
+ shade_muted_fill: Option<Color>,
98
+ shade_accent_fill: Option<Color>,
99
+ shade_alt_fill: Option<Color>,
98
100
  highlight_fill: Option<Color>,
99
101
  highlight_text: Color,
100
102
  gridline_color: Color,
@@ -122,7 +124,9 @@ impl ParsedPalette {
122
124
  total_text: parse_hex_color("total_text", &p.total_text)?,
123
125
  input_text: parse_hex_color("input_text", &p.input_text)?,
124
126
  formula_text: parse_hex_color("formula_text", &p.formula_text)?,
125
- zebra_fill: parse_fill_color("zebra_fill", &p.zebra_fill)?,
127
+ shade_muted_fill: parse_fill_color("shade_muted_fill", &p.shade_muted_fill)?,
128
+ shade_accent_fill: parse_fill_color("shade_accent_fill", &p.shade_accent_fill)?,
129
+ shade_alt_fill: parse_fill_color("shade_alt_fill", &p.shade_alt_fill)?,
126
130
  highlight_fill: parse_fill_color("highlight_fill", &p.highlight_fill)?,
127
131
  highlight_text: parse_hex_color("highlight_text", &p.highlight_text)?,
128
132
  gridline_color: parse_hex_color("gridline_color", &p.gridline_color)?,
@@ -184,6 +188,8 @@ pub struct FormatCache {
184
188
  title_format: Format,
185
189
  subtitle_format: Format,
186
190
  super_header_format: Format,
191
+ col_header_upper_descriptor_format: Format,
192
+ col_header_upper_measure_format: Format,
187
193
  col_header_descriptor_format: Format,
188
194
  col_header_measure_format: Format,
189
195
  section_header_format: Format,
@@ -239,15 +245,17 @@ impl FormatCache {
239
245
  }
240
246
 
241
247
  // 4. Column Header (Descriptor - Left Aligned)
242
- let mut col_header_descriptor_format = Format::new()
248
+ let mut col_header_upper_descriptor_format = Format::new()
243
249
  .set_font_name(&theme.typography.font_family)
244
250
  .set_font_size(theme.typography.col_header_size)
245
251
  .set_bold()
246
252
  .set_font_color(palette.col_header_text)
247
253
  .set_align(FormatAlign::Left);
248
254
  if let Some(fill) = palette.col_header_fill {
249
- col_header_descriptor_format = col_header_descriptor_format.set_background_color(fill);
255
+ col_header_upper_descriptor_format =
256
+ col_header_upper_descriptor_format.set_background_color(fill);
250
257
  }
258
+ let mut col_header_descriptor_format = col_header_upper_descriptor_format.clone();
251
259
  if theme.borders.header_border_bottom != BorderStyle::None {
252
260
  col_header_descriptor_format = col_header_descriptor_format
253
261
  .set_border_bottom(translate_border_style(theme.borders.header_border_bottom))
@@ -255,15 +263,17 @@ impl FormatCache {
255
263
  }
256
264
 
257
265
  // 5. Column Header (Measure - Right Aligned)
258
- let mut col_header_measure_format = Format::new()
266
+ let mut col_header_upper_measure_format = Format::new()
259
267
  .set_font_name(&theme.typography.font_family)
260
268
  .set_font_size(theme.typography.col_header_size)
261
269
  .set_bold()
262
270
  .set_font_color(palette.col_header_text)
263
271
  .set_align(FormatAlign::Right);
264
272
  if let Some(fill) = palette.col_header_fill {
265
- col_header_measure_format = col_header_measure_format.set_background_color(fill);
273
+ col_header_upper_measure_format =
274
+ col_header_upper_measure_format.set_background_color(fill);
266
275
  }
276
+ let mut col_header_measure_format = col_header_upper_measure_format.clone();
267
277
  if theme.borders.header_border_bottom != BorderStyle::None {
268
278
  col_header_measure_format = col_header_measure_format
269
279
  .set_border_bottom(translate_border_style(theme.borders.header_border_bottom))
@@ -290,6 +300,8 @@ impl FormatCache {
290
300
  title_format,
291
301
  subtitle_format,
292
302
  super_header_format,
303
+ col_header_upper_descriptor_format,
304
+ col_header_upper_measure_format,
293
305
  col_header_descriptor_format,
294
306
  col_header_measure_format,
295
307
  section_header_format,
@@ -328,15 +340,21 @@ impl FormatCache {
328
340
  };
329
341
 
330
342
  // 3. Background Fill Precedence:
331
- // is_highlight > total > subtotal > data
343
+ // is_highlight > total (or shade) > subtotal (or shade) > shade > data
344
+ let shade_fill = match key.role.shade {
345
+ Some(Shade::Muted) => self.palette.shade_muted_fill,
346
+ Some(Shade::Accent) => self.palette.shade_accent_fill,
347
+ Some(Shade::Alt) => self.palette.shade_alt_fill,
348
+ Some(Shade::None) | None => None,
349
+ };
332
350
  let fill = if key.role.is_highlight {
333
351
  self.palette.highlight_fill
334
352
  } else if key.role.horizontal.is_total() || key.role.vertical.is_total() {
335
- self.palette.total_fill
353
+ self.palette.total_fill.or(shade_fill)
336
354
  } else if key.role.horizontal.is_subtotal() || key.role.vertical.is_subtotal() {
337
- self.palette.subtotal_fill
355
+ self.palette.subtotal_fill.or(shade_fill)
338
356
  } else {
339
- self.palette.data_fill
357
+ shade_fill.or(self.palette.data_fill)
340
358
  };
341
359
  if let Some(fill_color) = fill {
342
360
  format = format.set_background_color(fill_color);
@@ -453,6 +471,16 @@ impl FormatCache {
453
471
  self.super_header_format.clone()
454
472
  }
455
473
 
474
+ /// Returns the upper-tier stacked column header format (descriptor or measure, without bottom border).
475
+ #[must_use]
476
+ pub fn col_header_upper_format(&self, is_descriptor: bool) -> Format {
477
+ if is_descriptor {
478
+ self.col_header_upper_descriptor_format.clone()
479
+ } else {
480
+ self.col_header_upper_measure_format.clone()
481
+ }
482
+ }
483
+
456
484
  /// Returns the column header format (descriptor or measure).
457
485
  #[must_use]
458
486
  pub fn col_header_format(&self, is_descriptor: bool) -> Format {
@@ -463,6 +491,12 @@ impl FormatCache {
463
491
  }
464
492
  }
465
493
 
494
+ /// Returns true if the theme defines a background fill for column headers.
495
+ #[must_use]
496
+ pub fn has_col_header_fill(&self) -> bool {
497
+ self.palette.col_header_fill.is_some()
498
+ }
499
+
466
500
  /// Returns the section header format with optional indentation level.
467
501
  #[must_use]
468
502
  pub fn section_header_format(&self, indent: Option<u8>) -> Format {