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)

Open fmstyle/web/index.html in any browser (or host it as a static page). 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-0.3.1.tar.gz (49.5 kB view details)

Uploaded Source

Built Distribution

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

fmstyle-0.3.1-py3-none-any.whl (44.8 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for fmstyle-0.3.1.tar.gz
Algorithm Hash digest
SHA256 3007757462845e4121d71d467ffeae05063859d681d66ea15ffd6a586d849070
MD5 924dbd87a74cc33ae35a6380df351108
BLAKE2b-256 eabdcddae02169cbc353c48229ca68b68495b07a0315f97914862a8fd4859477

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for fmstyle-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f8bc63954a720929a348d8aa02b744e2bda61c77cea56cd033791b204fda52cc
MD5 512d493cbbe22341ad3dbffbcbf1ef91
BLAKE2b-256 54974371bb7bdd050fa599ba29bcd00d51909cd02dd101f4b332bf51b365072b

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