Skip to main content

Deterministic formatter and shareable style engine for FileMaker calculations

Project description

fmstyle

A deterministic formatter and shareable style engine for FileMaker code. Think prettier / gofmt for FileMaker calculations: a team or solo developer defines their house style once, and every calculation — written by a human or by an LLM — comes out formatted the same way, every time.

Sibling of fmsonar (same philosophy: pure-stdlib Python core, zero dependencies, nothing leaves the machine).

Why

  1. Consistency is a style pack, not a habit. FileMaker style usually lives in people's heads or in prose guidelines. fmstyle splits a guideline into its two natural halves:
    • the mechanical half (fmstyle.json) — indentation, Let/While shape, line width, naming patterns — enforced deterministically by this tool;
    • the advisory half — philosophy, intent, naming semantics — prose that lives alongside it, written for humans and for LLMs to read.
  2. The LLM era is coming to FileMaker. Claris has signalled LLM/agent integration (Claude Code extensions and other models coding inside FileMaker). Prompting a model to "always format Let like this" mostly works; a deterministic post-pass always does. fmstyle is the harness: the model writes the logic, fmstyle format fixes the shape, fmstyle lint checks the rules. Useful today (paste / pipe / pre-commit), ready for a future agent integration.
  3. Safety you can trust. A formatter that can silently change semantics is worthless. fmstyle verifies that the output re-tokenizes to the exact same code tokens and comments as the input and refuses to emit anything otherwise (FormatSafetyError). If it can't parse an expression, it changes nothing.

What works today

  • Works on any calculation — Let, While, Case, If, Substitute, ExecuteSQL, JSON functions, custom functions, plain operator expressions… (Let/While simply have the strictest mandatory shapes). Script steps are not parsed yet — see roadmap.
  • Full tokenizer + parser + printer for FileMaker calculation expressions: operators (incl. ≤ ≥ ≠ ¶), strings with escapes, field names with spaces (Invoice Lines::Amount 2), ${quoted names}, $var / $$var, // and nestable /* */ comments, Substitute-style [...] argument lists.
  • Configurable Let / While / Case layout, width-aware inline-vs-exploded decisions, comment preservation.
  • Deterministic + idempotent (format(format(x)) == format(x), tested).
  • Token-preservation safety check on every format.
  • Configurable via fmstyle.json (indent, width, blank lines, keyword casing, result-variable name, naming pattern) — including per-function layout rules: any function name can be given its own shape (layout) and explode behaviour (multiline) under "functions".
  • Lint rules: let-explicit-result, variable-naming (more to come).
  • CLI: fmstyle format (stdin/files, --write, --check) and fmstyle lint — CI- and pre-commit-ready, exit codes included.

Install

pipx install fmstyle        # or: pip install fmstyle

Pure standard library, Python 3.10+, no dependencies.

Web app (no install)

Live at fmstyle.dev — or open fmstyle/web/index.html locally. Paste a calculation, it formats live, copy the result. Load your fmstyle.json to format in your house style; quick controls for indent and width. Everything runs entirely client-side — nothing is uploaded (no network requests, no CDN, no analytics).

The page embeds a JS port of the Python engine. Parity is enforced by fixtures: tests/gen_parity_fixtures.py renders every case through the Python reference, node tests/test_parity.mjs replays them through the JS engine extracted from the HTML and requires byte-identical output (formatting + lint findings).

Use it from your AI assistant (Claude Code skill)

Same three-way engine as fmsonar: one deterministic core, reachable from the browser, the shell, and an AI assistant. The package ships a Claude Code skill so the assistant supplies the logic and fmstyle supplies the shape:

fmstyle install-skill       # copies the skill to ~/.claude/skills/fmstyle
fmstyle install-skill --check   # freshness check after an upgrade

Once installed, when the assistant writes or edits a FileMaker calculation it runs it through fmstyle format before presenting it, so the output matches the house style rather than approximating it — and the token-safe check means it can trust the result without re-reading it for correctness.

Usage

# format from clipboard / stdin
pbpaste | fmstyle format | pbcopy

# format files, fail CI when not formatted
fmstyle format --check calcs/*.fmcalc

# guideline checks
fmstyle lint mycalc.fmcalc

# as a library (this is what an LLM harness calls)
python3 -c "from fmstyle import format_calc; print(format_calc('Let([x=1;result=x];result)'))"

Configuration (fmstyle.json)

A style pack has two honest halves — formatting (top level: mechanical layout, every team has some answer and any answer is valid) and lint (opinionated practice rules, each individually opt-in):

{
  "indent": "tab",
  "width": 96,
  "let_blank_lines": false,
  "keyword_case": "lower",
  "comments": "preserve",
  "wrap": { "operator_position": "trailing" },
  "spacing": {
    "inside_parens": true,
    "before_paren": true,
    "inside_brackets": true,
    "before_semicolon": true,
    "around_operators": true
  },
  "functions": {
    "let":   { "layout": "let",   "multiline": "always" },
    "while": { "layout": "while", "multiline": "always" },
    "case":  { "layout": "pairs" },
    "jsonsetelement": { "layout": "leading", "multiline": "always" }
  },
  "lint": {
    "let-explicit-result": { "result_name": "result" },
    "variable-naming":     { "pattern": "^[_a-z][A-Za-z0-9]*$" }
  }
}

With no lint section, no practice rules run: bare fmstyle formats your code but has no opinions you didn't give it. Prefer a calculation as the Let result instead of an explicit result variable? That's a valid style — just don't enable let-explicit-result. Wrapped operators at line ends ("a" &) instead of line starts? wrap.operator_position: "trailing". Tight parens (If(x; y))? Turn off the spacing pads. Inline comments moved above the code? comments: "above".

The full dimension taxonomy — every knob, its values, and which dimensions are (for now) fixed — lives in fmstyle/skill/style-pack.md, which doubles as the authoring instructions an AI assistant follows to build a team's pack from their guide, their code samples, or a short interview. An org's pack is just this JSON file, versioned in their repo. (0.2.x keys like result_name and space_before_semicolon still work as legacy shorthands.)

Presets

Named, ready-made style packs — pick one in the web app's toolbar or via fmstyle --preset <name> (a --config file overrides preset keys; fmstyle presets lists them):

Preset Highlights
oogi 4-space indent, blank-line Let blocks, leading-semicolon JSONSetElement; lint: explicit result, camelCase locals
compact tab indent, compact Let blocks (no blank lines), no lint opinions

Adding a preset is a PR with one dict in fmstyle/presets.py (and the matching entry in the web app). Presets should describe a real, adoptable convention.

Community edition (vision). The end game is a community preset governed in the open: proposals per rule ("blank lines in Let: yes/no"), public voting, a versioned result. If enough teams adopt it, FileMaker gets what gofmt gave Go — one format, fewer debates. The deterministic engine is the prerequisite; the governance can start as GitHub issues + reactions.

Per-function rules — every FileMaker (or custom) function name can get its own entry under "functions", so how each call formats is a config edit, not a code change:

Option Values Meaning
layout args (default) one argument per line when exploded
pairs condition ; result pairs per line (Case-style)
let / while the mandatory Let / While block shapes
leading first arg on the header line, leading-semicolon rest (JSONSetElement)
multiline auto (default) explode only when the call exceeds width
always always explode, even when it would fit

(force_multiline: [...] is still accepted as a legacy shorthand for multiline: "always".)

Validated against real solutions

tools/corpus_audit.py replays every calculation from a fmsonar SQLite database through the formatter. Measured on two independent production solutions (a 4-file and a 9-file multi-file solution), ~89–95% of calculations format cleanly; the exact figure varies by solution and by how the DDR was exported.

The remainder is almost entirely DDR-export artifacts (several calcs concatenated into one layout-object entry, <Field Missing> placeholders) which the formatter correctly refuses rather than mangles. The audit drove real grammar fixes: ~/#/dotted/unicode names ($$~DISABLETRIGGERS, dotted custom-function namespaces, Fee 5¢ Surcharge), repetition references (field[11]), x = not y assignments, trailing semicolons (Case ( a ; b ; )), and commented-out calcs. It also inventories which built-in and custom functions a solution uses (--functions) — input for choosing per-function rules.

Roadmap

  • Calculation formatter + first lint rules + CLI.
  • Web app — single client-side HTML page, JS port parity-tested byte-for-byte against Python. Paste → format → copy; load your fmstyle.json; live lint; light/dark.
  • LLM harness packaging — a Claude Code skill (fmstyle install-skill) and PyPI packaging (the wheel bundles the web app + skill).
  • Copy as FM object — wrap output as fmxmlsnippet where applicable (custom functions, Set Variable steps) so paste lands as a real object.
  • Script-step formatting & linting — via Save-as-XML: script naming, error-handling blocks, comment headers.
  • More lint rules — custom function headers, While counter conventions, magic-number detection, configurable per org.
  • Community preset + voting — public per-rule proposals and voting.
  • Whole-solution style report — pipe every calculation from a fmsonar SQLite DB through fmstyle lint for a solution-wide style/health report.

Known limitations

  • Calculation expressions only — no script steps yet (see roadmap).
  • Reserved/ambiguous names: a bare multi-word field reference is accepted verbatim; a field literally named like a keyword (and) needs ${and}.
  • Comments in unusual positions (e.g. between an operator and its operand) are preserved but may move to the nearest line boundary; if preservation is ever impossible the tool refuses rather than guesses.
  • Function names are kept in the author's casing (no canonical-case rewrite yet).

Contributing

Two engines are kept in lockstep: the Python reference (fmstyle/) and a JS port embedded in fmstyle/web/index.html (between the fmstyle-engine-start/end markers). After changing either, regenerate fixtures and run both suites — see CONTRIBUTING.md.

Tech stack

Concern Choice Why
Language Python 3.10+, stdlib only Zero install friction
Parsing hand-written lexer + recursive descent FM calc grammar is small; full control over verbatim tokens
Safety token-stream equality check the formatter can't change the computation without the check catching it
Config JSON dataclass trivially shareable as an org style pack

Project structure

fmstyle/
├── README.md
├── LICENSE
├── CONTRIBUTING.md
├── pyproject.toml
├── fmstyle/
│   ├── web/index.html  # zero-install client-side web app (JS engine + UI)
│   ├── skill/SKILL.md  # packaged Claude Code skill
│   ├── __init__.py     # format_calc / lint_calc API + safety check
│   ├── lexer.py        # verbatim tokenizer (comments, spaced names, ${...})
│   ├── parser.py       # recursive descent -> small AST
│   ├── printer.py      # deterministic layout engine (Let/While/Case shapes)
│   ├── config.py       # Style dataclass <- fmstyle.json
│   ├── presets.py      # named style packs
│   ├── rules.py        # lint rules
│   └── cli.py          # fmstyle format / lint / presets / install-skill
├── tools/
│   └── corpus_audit.py # replay every calc from a fmsonar DB through the formatter
└── tests/
    ├── test_fmstyle.py          # exact-output, idempotence, safety, lint tests
    ├── gen_parity_fixtures.py   # Python reference -> parity_fixtures.json
    ├── parity_fixtures.json     # generated JS<->Python parity cases
    └── test_parity.mjs          # node: JS engine must match Python byte-for-byte

License

MIT — see LICENSE.

FileMaker is a trademark of Claris International Inc. This project is independent and not affiliated with or endorsed by Claris.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fmstyle-1.0.0.tar.gz (49.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fmstyle-1.0.0-py3-none-any.whl (44.9 kB view details)

Uploaded Python 3

File details

Details for the file fmstyle-1.0.0.tar.gz.

File metadata

  • Download URL: fmstyle-1.0.0.tar.gz
  • Upload date:
  • Size: 49.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.6

File hashes

Hashes for fmstyle-1.0.0.tar.gz
Algorithm Hash digest
SHA256 e863483e984e38d9fbe7800a4d7e5d51b1aacd8239b732e7df42776bdfeb5bf0
MD5 c2c439dcf2071681be676aa1c60d43cc
BLAKE2b-256 91232e1ee7328eb6a35be29bcac268e17170678ce32ed1d78bff1ff5469cda20

See more details on using hashes here.

File details

Details for the file fmstyle-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: fmstyle-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 44.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.6

File hashes

Hashes for fmstyle-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 15355eac165e97942f2e42ca1e14a322cacb1eb7b26080dce2154ecf89f9bc3f
MD5 a3d24bd99288bf31cdae993d2c4f2424
BLAKE2b-256 56e3eacfd308a8e95d4efdadbdd05fa6c3f16739c2fc04cad6d65519c98da67a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page