Sign in

    sheetsentry

    Static analysis and dependency tracing for Excel models, written in MoonBit.

    excel
    xlsx
    audit
    spreadsheet
    static-analysis
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    4 days ago
    Downloads
    29

    #SheetSentry

    Static analysis and dependency tracing for Excel models, written in MoonBit.

    SheetSentry reads .xlsx workbooks, parses formula references across sheets, builds a cell dependency graph, and reports model risks that are hard to see in Excel: error values, inconsistent formulas, hardcoded constants, volatile functions, hidden content, and circular references.

    It is designed for finance, audit, FP&A, and data teams that want spreadsheet checks to run like code checks: deterministic, scriptable, and CI-friendly.

    #Why

    Spreadsheet errors are expensive and easy to miss:

    • a formula is copied down a column but one row points at the wrong cell;
    • a model output depends on a hidden sheet;
    • a hardcoded growth rate is buried inside a formula;
    • a circular reference only appears after a workbook is opened;
    • an error value is cached behind a formula and never noticed.

    Excel's built-in Trace Precedents / Trace Dependents are useful, but they are manual, single-cell, and not available in CI. SheetSentry makes the same ideas scriptable.

    #Status

    MVP, implemented in MoonBit. The current release supports:

    • XLSX reading through moonbitlang/mbtexcel
    • cross-sheet formula reference extraction
    • dependency graph construction
    • six audit rules plus a dynamic-reference warning
    • terminal, JSON, SARIF, and self-contained HTML reports
    • trace for precedents and dependents
    • CI-friendly --fail-on exit codes

    #Quick start

    Requires the MoonBit toolchain.

    moon update moon check --target native moon test --target native moon run --target native cmd/main -- audit fixtures/bad_model.xlsx

    Expected output:

    SheetSentry: fixtures/bad_model.xlsx sheets: 5 formula cells: 14 findings: 11 high: 4 medium: 6 low: 1 [medium] SS005 Opex!C6 Formula lives on a hidden row. [medium] SS003 Summary!B2 Formula embeds numeric constant 1.05; prefer a named input cell. [high] SS001 Summary!B4 Cell contains error value #REF!. ...

    #CLI

    # Terminal report moon run --target native cmd/main -- audit model.xlsx # JSON moon run --target native cmd/main -- audit model.xlsx --format json --out findings.json # SARIF for code scanning moon run --target native cmd/main -- audit model.xlsx --format sarif --out results.sarif # Self-contained HTML report moon run --target native cmd/main -- audit model.xlsx --format html --out report.html # Trace precedents (what this cell depends on) moon run --target native cmd/main -- trace model.xlsx --cell "Summary!B7" --direction precedents --depth 4 # Trace dependents (what breaks if this cell changes) moon run --target native cmd/main -- trace model.xlsx --cell "Inputs!B2" --direction dependents --depth 4 # Fail CI if any high-severity finding exists moon run --target native cmd/main -- audit model.xlsx --fail-on high

    #Rules

    IDSeverityRuleWhy it matters
    SS001HighError value#REF!, #DIV/0!, #VALUE!, #N/A, #NAME?, #NUM! silently poison downstream results.
    SS002MediumFormula inconsistencyOne cell in a copied row/column uses a different formula pattern.
    SS003MediumHardcoded constantA numeric constant is embedded in a formula instead of living in an input cell.
    SS004LowVolatile functionTODAY, NOW, RAND, RANDBETWEEN, OFFSET, INDIRECT, INFO, CELL make results time- or environment-dependent.
    SS005MediumHidden formulaFormulas on hidden sheets or hidden rows are easy to miss during review.
    SS006HighCircular referenceA dependency cycle makes calculation order undefined or unstable.
    SS007LowDynamic referenceINDIRECT / OFFSET prevent complete static dependency tracing.

    The rule catalog is intentionally small and deterministic. Every finding has a stable rule ID, severity, sheet, cell, message, optional formula, and related cells.

    #Reports

    • Terminal — fast review in a shell.
    • JSON — machine-readable for custom dashboards.
    • SARIF 2.1.0 — intended for GitHub Code Scanning and other SARIF consumers.
    • HTML — a single self-contained file with summary cards and a findings table; no network or external assets.

    #CI example

    - name: Audit Excel model run: | moon run --target native cmd/main -- \ audit model.xlsx --format sarif --out results.sarif --fail-on high - name: Upload SARIF if: always() uses: github/codeql-action/upload-sarif@v3 with: sarif_file: results.sarif

    The repository's own CI runs moon update, moon check, moon test, and moon build on every push.

    #Architecture

    flowchart LR XLSX[XLSX bytes] --> Reader[mbtexcel reader] Reader --> Cells[Formula cells + cached values + visibility] Cells --> Refs[Formula reference parser] Refs --> Graph[Cross-sheet dependency graph] Graph --> Rules[Rule engine] Cells --> Rules Rules --> Report[Terminal / JSON / SARIF / HTML] Graph --> Trace[Precedents / dependents]

    Core modules:

    • formula.mbt — A1 / range / sheet-qualified reference extraction, normalization, function names, numeric literals.
    • audit.mbt — workbook scan, dependency graph, rule engine, cycle detection.
    • trace.mbt — graph construction and BFS traversal for precedents/dependents.
    • report.mbt — terminal, JSON, SARIF, and HTML renderers.
    • cmd/main/main.mbt — CLI.

    #Testing

    moon test --target native

    Tests cover:

    • reference extraction including quoted sheet names and absolute references;
    • function-name and numeric-literal extraction;
    • formula normalization across rows;
    • all six audit rules on fixtures/bad_model.xlsx;
    • a clean workbook with zero findings;
    • report rendering;
    • precedents and dependents tracing;
    • fail thresholds.

    Fixtures are generated reproducibly with:

    PYTHONPATH=../tools/pylib python3 scripts/make_fixtures.py

    The Python script is a test-data generator only; the analyzer itself is MoonBit.

    #Limitations

    • XLSX only (no .xls, .ods, or Google Sheets yet).
    • Formula references are extracted statically; INDIRECT and OFFSET are reported but cannot be fully resolved.
    • Structured references (Table1[Column]) and external workbook links are not yet part of the dependency graph.
    • Cycle detection currently reports simple dependency cycles; complex strongly connected components are on the roadmap.
    • The rule engine is heuristic. Use the rule catalog and CI thresholds to fit your team's conventions.

    #Roadmap

    • Baseline file to suppress known findings and fail only on new ones.
    • More rules: orphan calculations, external links, merged cells in data regions, inconsistent units, over-long formulas.
    • Better cycle reporting with strongly connected components.
    • --fix for safe, deterministic rewrites.
    • WASM build so the report can run fully in a browser.

    #License

    Apache-2.0. See LICENSE.

    #Hackathon

    Built for the 2026 MoonBit Hackathon, October season.

    AuditReport

    pub(all) struct AuditReport {
    file : String
    sheets : Array[String]
    formula_cells : Int
    findings : Array[Finding]
    }

    CellRef

    pub(all) struct CellRef {
    start : Int
    end : Int
    sheet : String?
    row : Int
    col : Int
    end_row : Int
    end_col : Int
    abs_row : Bool
    abs_col : Bool
    }

    DependencyGraph

    pub(all) struct DependencyGraph {
    edges : Map[String, Array[String]]
    reverse : Map[String, Array[String]]
    }

    DependencyGraph::new

    Finding

    pub(all) struct Finding {
    rule_id : String
    severity : Severity
    sheet : String
    cell : String
    formula : String?
    message : String
    related : Array[String]
    }

    FormulaRefs

    pub(all) struct FormulaRefs {
    cells : Array[CellRef]
    names : Array[String]
    dynamic : Bool
    }

    Severity

    pub(all) enum Severity {
    High
    Medium
    Low
    }

    Severity::label

    fn Severity::label(self : Severity) -> String

    WorkbookSummary

    pub(all) struct WorkbookSummary {
    sheets : Array[String]
    formula_cells : Int
    stored_cells : Int
    }

    audit_bytes

    fn audit_bytes(bytes : Bytes, file : String) -> AuditReport raise
    XlsxError

    build_graph_bytes

    fn build_graph_bytes(bytes : Bytes) -> DependencyGraph raise
    XlsxError

    count_severity_public

    fn count_severity_public(report : AuditReport, severity : Severity) -> Int

    extract_refs

    fn extract_refs(formula : StringView, current_sheet : StringView) -> FormulaRefs

    function_names

    fn function_names(formula : StringView) -> Array[String]

    normalize_formula

    fn normalize_formula(formula : StringView, current_sheet : StringView, row : Int, col : Int) -> String

    numeric_literals

    fn numeric_literals(formula : StringView) -> Array[String]

    should_fail

    fn should_fail(report : AuditReport, threshold : String) -> Bool

    summarize_bytes

    fn summarize_bytes(bytes : Bytes) -> WorkbookSummary raise
    XlsxError

    to_html

    fn to_html(report : AuditReport) -> String

    to_json

    fn to_json(report : AuditReport) -> String

    to_sarif

    fn to_sarif(report : AuditReport) -> String

    to_terminal

    fn to_terminal(report : AuditReport) -> String

    trace_bytes

    fn trace_bytes(bytes : Bytes, start : String, direction : String, max_depth : Int) -> Array[String] raise
    XlsxError