fmstyle 0.2.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.
fmstyle-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,283 @@
1
+ Metadata-Version: 2.4
2
+ Name: fmstyle
3
+ Version: 0.2.0
4
+ Summary: Deterministic formatter and shareable style engine for FileMaker calculations
5
+ Author: OOGI BV
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/oogi-io/fm-code-formatter
8
+ Keywords: filemaker,formatter,calculation,style,lint
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+
12
+ # fm-style
13
+
14
+ **A deterministic formatter and shareable style engine for FileMaker code.**
15
+ Think *prettier / gofmt for FileMaker calculations*: any company or solo developer
16
+ defines their house style once, and every calculation — written by a human or by an
17
+ LLM — comes out formatted exactly the same way, every time.
18
+
19
+ Sibling of [fm-ddr-analyzer](../fm-ddr-analyzer) (same philosophy: pure-stdlib
20
+ Python core, zero dependencies, client data never leaves the machine).
21
+
22
+ ## Why
23
+
24
+ 1. **Consistency is a style pack, not a habit.** Today FileMaker style lives in
25
+ people's heads or in prose guidelines. fm-style splits a guideline into its two
26
+ natural halves:
27
+ - the **mechanical half** (`fmstyle.json`) — indentation, Let/While shape, line
28
+ width, naming patterns — enforced *deterministically* by this tool;
29
+ - the **advisory half** (`guidelines.md`) — philosophy, intent, naming semantics —
30
+ written for humans **and for LLMs** to read.
31
+ 2. **The LLM era is coming to FileMaker.** Claris is planning LLM/agent integration
32
+ (Claude Code extensions and other models coding inside FileMaker). Prompting a
33
+ model to "always format Let like this" works 95% of the time; a deterministic
34
+ post-pass works 100% of the time. fm-style is the harness: the model writes the
35
+ logic, `fmstyle format` guarantees the shape, `fmstyle lint` guarantees the rules.
36
+ Useful today (paste / pipe / pre-commit), ready for tomorrow (skill / MCP tool
37
+ inside the Claris integration).
38
+ 3. **Safety you can trust.** A formatter that can silently change semantics is
39
+ worthless. fm-style **verifies that the output re-tokenizes to the exact same
40
+ code tokens and comments as the input** and refuses to emit anything otherwise
41
+ (`FormatSafetyError`). If it can't parse an expression, it changes nothing.
42
+
43
+ ## What works today (v0.1)
44
+
45
+ - **Works on any calculation** — Let, While, Case, If, Substitute, ExecuteSQL,
46
+ JSON functions, custom functions, plain operator expressions… (Let/While simply
47
+ have the strictest mandatory shapes). Script *steps* are not parsed yet — see
48
+ roadmap.
49
+ - Full tokenizer + parser + printer for FileMaker calculation expressions:
50
+ operators (incl. `≤ ≥ ≠ ¶`), strings with escapes, field names **with spaces**
51
+ (`Invoice Lines::Amount 2`), `${quoted names}`, `$var` / `$$var`, `//` and
52
+ nestable `/* */` comments, `Substitute`-style `[...]` argument lists.
53
+ - **Style-guide-exact Let and While layout** (mandatory shapes from the
54
+ [FileMaker Development Style Guide](../../filemaker/source_of_truth/style/filemaker_development_style_guide.md)),
55
+ Case pair layout, width-aware inline-vs-exploded decisions, comment preservation.
56
+ - Deterministic + **idempotent** (`format(format(x)) == format(x)`, tested).
57
+ - Token-preservation safety check on every format.
58
+ - Configurable via `fmstyle.json` (indent, width, blank lines, keyword casing,
59
+ result-variable name, naming pattern) — including **per-function layout rules**:
60
+ any function name can be given its own shape (`layout`) and explode behaviour
61
+ (`multiline`) under `"functions"`.
62
+ - Lint rules: `let-explicit-result`, `variable-naming` (more to come).
63
+ - CLI: `fmstyle format` (stdin/files, `--write`, `--check`) and `fmstyle lint` —
64
+ CI- and pre-commit-ready, exit codes included.
65
+
66
+ ## Web app (no install)
67
+
68
+ Open `fmstyle/web/index.html` in any browser (or host it as a static page). Paste a
69
+ calculation, it formats live, copy the result. Load your organisation's
70
+ `fmstyle.json` to format in your house style; quick controls for indent and width.
71
+ Everything runs **entirely client-side — nothing is uploaded** (same privacy story
72
+ as the DDR analyzer's web app).
73
+
74
+ The page embeds a JS port of the Python engine. Parity is enforced by fixtures:
75
+ `tests/gen_parity_fixtures.py` renders every case through the Python reference,
76
+ `node tests/test_parity.mjs` replays them through the JS engine extracted from the
77
+ HTML and requires **byte-identical** output (formatting + lint findings).
78
+
79
+ ## Use it from your AI assistant (Claude Code skill)
80
+
81
+ Same three-way engine as [fmsonar](../fm-ddr-analyzer): one deterministic core,
82
+ reachable from the browser, the shell, **and an AI assistant**. The package ships
83
+ a Claude Code skill so the assistant supplies the logic and `fmstyle` supplies the
84
+ guaranteed shape:
85
+
86
+ ```bash
87
+ pipx install fmstyle # (PyPI) or: pip install fmstyle
88
+ fmstyle install-skill # copies the skill to ~/.claude/skills/fmstyle
89
+ fmstyle install-skill --check # freshness check after an upgrade
90
+ ```
91
+
92
+ Once installed, when the assistant writes or edits a FileMaker calculation it
93
+ runs it through `fmstyle format` before presenting it, so the output matches the
94
+ house style exactly rather than approximately — and the token-safe guarantee
95
+ means it can trust the result without re-reading it for correctness. This is the
96
+ piece that anticipates Claris's LLM/agent integration: the model writes the calc,
97
+ `fmstyle` makes it house-style, deterministically.
98
+
99
+ ## Usage
100
+
101
+ ```bash
102
+ cd projects/fm-style
103
+
104
+ # format from clipboard / stdin
105
+ pbpaste | python3 -m fmstyle.cli format | pbcopy
106
+
107
+ # format files, fail CI when not formatted
108
+ python3 -m fmstyle.cli format --check calcs/*.fmcalc
109
+
110
+ # guideline checks
111
+ python3 -m fmstyle.cli lint mycalc.txt
112
+
113
+ # as a library (this is what an LLM harness calls)
114
+ python3 -c "from fmstyle import format_calc; print(format_calc('Let([x=1;result=x];result)'))"
115
+ ```
116
+
117
+ ## Configuration (`fmstyle.json`)
118
+
119
+ ```json
120
+ {
121
+ "indent": "tab",
122
+ "width": 96,
123
+ "let_blank_lines": true,
124
+ "lowercase_keywords": true,
125
+ "space_before_semicolon": true,
126
+ "result_name": "result",
127
+ "local_variable_pattern": "^[_a-z][A-Za-z0-9]*$",
128
+ "functions": {
129
+ "let": { "layout": "let", "multiline": "always" },
130
+ "while": { "layout": "while", "multiline": "always" },
131
+ "case": { "layout": "pairs" },
132
+ "if": { "multiline": "always" }
133
+ }
134
+ }
135
+ ```
136
+
137
+ Defaults implement the Thomas DS style guide with 4-space indent. An organisation's
138
+ pack is just this file plus their `guidelines.md`, versioned in their repo.
139
+
140
+ ### Presets
141
+
142
+ Named, ready-made style packs — pick one in the web app's toolbar or via
143
+ `fmstyle --preset <name>` (a `--config` file overrides preset keys;
144
+ `fmstyle presets` lists them):
145
+
146
+ | Preset | Source | Highlights |
147
+ |---|---|---|
148
+ | `oogi` | OOGI BV FileMaker Development Style Guide | 4-space indent, blank-line Let blocks, explicit `result`, leading-semicolon JSONSetElement (§8.1) |
149
+ | `soliant` | *draft* — derived from observed AMPS conventions | tab indent, compact Let blocks, `_camelCase` locals |
150
+
151
+ Presets must be **derived from a firm's real conventions, not invented** — the
152
+ Soliant preset is marked draft until someone at Soliant blesses it. Adding a
153
+ consultancy's preset is a PR with one dict in `fmstyle/presets.py` (+ the same
154
+ entry in the web app).
155
+
156
+ **Community edition (vision).** The end game is a `community` preset governed in
157
+ the open: proposals per rule ("blank lines in Let: yes/no"), public voting, a
158
+ versioned result (`community-2027.1`). If enough firms adopt it, FileMaker gets
159
+ what gofmt gave Go — one format, zero debates. The deterministic engine is the
160
+ prerequisite; the governance can start as GitHub issues + reactions before it
161
+ needs anything fancier.
162
+
163
+ **Per-function rules** — every FileMaker (or custom) function name can get its own
164
+ entry under `"functions"`, so how *each* call formats is a config edit, not a code
165
+ change:
166
+
167
+ | Option | Values | Meaning |
168
+ |---|---|---|
169
+ | `layout` | `args` (default) | one argument per line when exploded |
170
+ | | `pairs` | condition ; result pairs per line (Case-style) |
171
+ | | `let` / `while` | the mandatory Let / While block shapes |
172
+ | `multiline` | `auto` (default) | explode only when the call exceeds `width` |
173
+ | | `always` | always explode, even when it would fit |
174
+
175
+ (`force_multiline: [...]` is still accepted as a legacy shorthand for
176
+ `multiline: "always"`.)
177
+
178
+ ## Validated against real solutions
179
+
180
+ `tools/corpus_audit.py` replays **every calculation** from an
181
+ [fm-ddr-analyzer](../fm-ddr-analyzer) SQLite database through the formatter:
182
+
183
+ | Solution | Calculations | Format cleanly |
184
+ |---|---|---|
185
+ | tjouly (4-file DDR, 2026-05) | 47,665 | **94.8%** |
186
+ | AMPS / Sunsolar (9-file DDR, 2026-06) | 88,401 | **94.2%** |
187
+
188
+ The remainder is almost entirely DDR-export artifacts (several calcs
189
+ concatenated into one layout-object entry, `<Field Missing>` placeholders) which
190
+ the formatter correctly *refuses* rather than mangles. The audit drove real
191
+ grammar fixes: `~`/`#`/dotted/unicode names (`$$~DISABLETRIGGERS`,
192
+ `#ScriptResultJSON`, `Session.GetValue`, `Rep 5¢ Commission`), repetition
193
+ references (`field[11]`), `x = not y` assignments, trailing semicolons
194
+ (`Case ( a ; b ; )`), and commented-out calcs. It also inventories which
195
+ built-in and custom functions a solution actually uses (`--functions`) — input
196
+ for choosing per-function rules.
197
+
198
+ ## One tool, three fronts (convergence plan)
199
+
200
+ Three FileMaker tools now share one DNA (stream Claris XML, pure stdlib, private
201
+ by design):
202
+
203
+ | Tool | Input | Output |
204
+ |---|---|---|
205
+ | [fm-ddr-analyzer](../fm-ddr-analyzer) | DDR XML | queryable SQLite + explorer |
206
+ | [accessibility-label-patcher](../soliant/spikes/accessibility-label-patcher) | SaXML | FMUpgradeTool patch |
207
+ | **fm-style** (this) | calculation text (later: SaXML/snippets) | formatted code + lint report |
208
+
209
+ Convergence happens in two layers, without merging repos prematurely:
210
+
211
+ 1. **Shared cores.** The calculation tokenizer built here is directly reusable by
212
+ the DDR analyzer (pretty-printing calcs in the explorer, structural search) and
213
+ by any SaXML-processing step. Extract to a shared package only when a second
214
+ consumer actually lands.
215
+ 2. **One web umbrella.** Like the DDR analyzer's zero-upload web app, fm-style gets
216
+ a client-side page (paste calc → formatted, org pack loadable from a JSON file).
217
+ The umbrella site (oogi.io) presents them as one FileMaker toolbelt; each tool
218
+ stays independently shippable underneath.
219
+
220
+ ## Roadmap
221
+
222
+ - [x] **v0.1 — calculation formatter + first lint rules + CLI** (this).
223
+ - [x] **Web app** — single client-side HTML page (`fmstyle/web/index.html`), JS
224
+ port parity-tested byte-for-byte against Python. Paste → format → copy;
225
+ load your `fmstyle.json`; live lint findings; light/dark.
226
+ - [ ] **Copy as FM object** — where applicable, wrap output as `fmxmlsnippet`
227
+ (custom functions, Set Variable steps) so paste lands as a real object.
228
+ - [x] **LLM harness packaging** — a Claude Code skill (`fmstyle install-skill`)
229
+ that makes the assistant auto-run `fmstyle format` on FileMaker calcs it
230
+ writes, plus PyPI-ready packaging (wheel bundles the web app + skill).
231
+ Next: publish to PyPI; optionally inject the org's `guidelines.md`.
232
+ - [ ] **Script-step formatting & linting** — via SaXML (the accessibility patcher
233
+ already proves parse→patch round-trips): script naming, error-handling
234
+ blocks, comment headers from the style guide.
235
+ - [ ] **More lint rules** — custom function headers, `While` counter conventions,
236
+ magic-number detection, configurable per org.
237
+ - [ ] **Community preset + voting** — public per-rule proposals and voting
238
+ (GitHub issues/reactions to start), producing a versioned `community`
239
+ preset that firms can adopt — one unified FileMaker format, eventually.
240
+ - [ ] **DDR synergy** — lint a whole solution by piping every calculation from an
241
+ fm-ddr-analyzer SQLite DB through `fmstyle lint` (style report for an entire
242
+ file, like a health report).
243
+
244
+ ## Known limitations (v0.1)
245
+
246
+ - Calculation expressions only — no script steps yet (see roadmap).
247
+ - Reserved/ambiguous names: a bare multi-word field reference is accepted
248
+ verbatim; a field literally named like a keyword (`and`) needs `${and}`.
249
+ - Comments in unusual positions (e.g. between an operator and its operand) are
250
+ preserved but may be moved to the nearest line boundary; if preservation is
251
+ ever impossible the tool refuses rather than guesses.
252
+ - Function names are kept in the author's casing (no canonical-case rewrite yet).
253
+
254
+ ## Tech stack
255
+
256
+ | Concern | Choice | Why |
257
+ |---|---|---|
258
+ | Language | Python 3.10+, stdlib only | Zero install friction, matches fm-ddr-analyzer |
259
+ | Parsing | hand-written lexer + recursive descent | FM calc grammar is small; full control over verbatim tokens |
260
+ | Safety | token-stream equality check | formatter can provably never change semantics |
261
+ | Config | JSON dataclass | trivially shareable as an "org style pack" |
262
+
263
+ ## Project structure
264
+
265
+ ```
266
+ fm-style/
267
+ ├── README.md
268
+ ├── pyproject.toml
269
+ ├── fmstyle/
270
+ │ ├── web/index.html # zero-install client-side web app (JS engine + UI)
271
+ │ ├── __init__.py # format_calc / lint_calc API + safety check
272
+ │ ├── lexer.py # verbatim tokenizer (comments, spaced names, ${...})
273
+ │ ├── parser.py # recursive descent -> small AST
274
+ │ ├── printer.py # deterministic layout engine (Let/While/Case shapes)
275
+ │ ├── config.py # Style dataclass <- fmstyle.json
276
+ │ ├── rules.py # lint rules
277
+ │ └── cli.py # fmstyle format / lint
278
+ └── tests/
279
+ ├── test_fmstyle.py # exact-output, idempotence, safety, lint tests
280
+ ├── gen_parity_fixtures.py # Python reference -> parity_fixtures.json
281
+ ├── parity_fixtures.json # generated JS<->Python parity cases
282
+ └── test_parity.mjs # node: JS engine must match Python byte-for-byte
283
+ ```
@@ -0,0 +1,272 @@
1
+ # fm-style
2
+
3
+ **A deterministic formatter and shareable style engine for FileMaker code.**
4
+ Think *prettier / gofmt for FileMaker calculations*: any company or solo developer
5
+ defines their house style once, and every calculation — written by a human or by an
6
+ LLM — comes out formatted exactly the same way, every time.
7
+
8
+ Sibling of [fm-ddr-analyzer](../fm-ddr-analyzer) (same philosophy: pure-stdlib
9
+ Python core, zero dependencies, client data never leaves the machine).
10
+
11
+ ## Why
12
+
13
+ 1. **Consistency is a style pack, not a habit.** Today FileMaker style lives in
14
+ people's heads or in prose guidelines. fm-style splits a guideline into its two
15
+ natural halves:
16
+ - the **mechanical half** (`fmstyle.json`) — indentation, Let/While shape, line
17
+ width, naming patterns — enforced *deterministically* by this tool;
18
+ - the **advisory half** (`guidelines.md`) — philosophy, intent, naming semantics —
19
+ written for humans **and for LLMs** to read.
20
+ 2. **The LLM era is coming to FileMaker.** Claris is planning LLM/agent integration
21
+ (Claude Code extensions and other models coding inside FileMaker). Prompting a
22
+ model to "always format Let like this" works 95% of the time; a deterministic
23
+ post-pass works 100% of the time. fm-style is the harness: the model writes the
24
+ logic, `fmstyle format` guarantees the shape, `fmstyle lint` guarantees the rules.
25
+ Useful today (paste / pipe / pre-commit), ready for tomorrow (skill / MCP tool
26
+ inside the Claris integration).
27
+ 3. **Safety you can trust.** A formatter that can silently change semantics is
28
+ worthless. fm-style **verifies that the output re-tokenizes to the exact same
29
+ code tokens and comments as the input** and refuses to emit anything otherwise
30
+ (`FormatSafetyError`). If it can't parse an expression, it changes nothing.
31
+
32
+ ## What works today (v0.1)
33
+
34
+ - **Works on any calculation** — Let, While, Case, If, Substitute, ExecuteSQL,
35
+ JSON functions, custom functions, plain operator expressions… (Let/While simply
36
+ have the strictest mandatory shapes). Script *steps* are not parsed yet — see
37
+ roadmap.
38
+ - Full tokenizer + parser + printer for FileMaker calculation expressions:
39
+ operators (incl. `≤ ≥ ≠ ¶`), strings with escapes, field names **with spaces**
40
+ (`Invoice Lines::Amount 2`), `${quoted names}`, `$var` / `$$var`, `//` and
41
+ nestable `/* */` comments, `Substitute`-style `[...]` argument lists.
42
+ - **Style-guide-exact Let and While layout** (mandatory shapes from the
43
+ [FileMaker Development Style Guide](../../filemaker/source_of_truth/style/filemaker_development_style_guide.md)),
44
+ Case pair layout, width-aware inline-vs-exploded decisions, comment preservation.
45
+ - Deterministic + **idempotent** (`format(format(x)) == format(x)`, tested).
46
+ - Token-preservation safety check on every format.
47
+ - Configurable via `fmstyle.json` (indent, width, blank lines, keyword casing,
48
+ result-variable name, naming pattern) — including **per-function layout rules**:
49
+ any function name can be given its own shape (`layout`) and explode behaviour
50
+ (`multiline`) under `"functions"`.
51
+ - Lint rules: `let-explicit-result`, `variable-naming` (more to come).
52
+ - CLI: `fmstyle format` (stdin/files, `--write`, `--check`) and `fmstyle lint` —
53
+ CI- and pre-commit-ready, exit codes included.
54
+
55
+ ## Web app (no install)
56
+
57
+ Open `fmstyle/web/index.html` in any browser (or host it as a static page). Paste a
58
+ calculation, it formats live, copy the result. Load your organisation's
59
+ `fmstyle.json` to format in your house style; quick controls for indent and width.
60
+ Everything runs **entirely client-side — nothing is uploaded** (same privacy story
61
+ as the DDR analyzer's web app).
62
+
63
+ The page embeds a JS port of the Python engine. Parity is enforced by fixtures:
64
+ `tests/gen_parity_fixtures.py` renders every case through the Python reference,
65
+ `node tests/test_parity.mjs` replays them through the JS engine extracted from the
66
+ HTML and requires **byte-identical** output (formatting + lint findings).
67
+
68
+ ## Use it from your AI assistant (Claude Code skill)
69
+
70
+ Same three-way engine as [fmsonar](../fm-ddr-analyzer): one deterministic core,
71
+ reachable from the browser, the shell, **and an AI assistant**. The package ships
72
+ a Claude Code skill so the assistant supplies the logic and `fmstyle` supplies the
73
+ guaranteed shape:
74
+
75
+ ```bash
76
+ pipx install fmstyle # (PyPI) or: pip install fmstyle
77
+ fmstyle install-skill # copies the skill to ~/.claude/skills/fmstyle
78
+ fmstyle install-skill --check # freshness check after an upgrade
79
+ ```
80
+
81
+ Once installed, when the assistant writes or edits a FileMaker calculation it
82
+ runs it through `fmstyle format` before presenting it, so the output matches the
83
+ house style exactly rather than approximately — and the token-safe guarantee
84
+ means it can trust the result without re-reading it for correctness. This is the
85
+ piece that anticipates Claris's LLM/agent integration: the model writes the calc,
86
+ `fmstyle` makes it house-style, deterministically.
87
+
88
+ ## Usage
89
+
90
+ ```bash
91
+ cd projects/fm-style
92
+
93
+ # format from clipboard / stdin
94
+ pbpaste | python3 -m fmstyle.cli format | pbcopy
95
+
96
+ # format files, fail CI when not formatted
97
+ python3 -m fmstyle.cli format --check calcs/*.fmcalc
98
+
99
+ # guideline checks
100
+ python3 -m fmstyle.cli lint mycalc.txt
101
+
102
+ # as a library (this is what an LLM harness calls)
103
+ python3 -c "from fmstyle import format_calc; print(format_calc('Let([x=1;result=x];result)'))"
104
+ ```
105
+
106
+ ## Configuration (`fmstyle.json`)
107
+
108
+ ```json
109
+ {
110
+ "indent": "tab",
111
+ "width": 96,
112
+ "let_blank_lines": true,
113
+ "lowercase_keywords": true,
114
+ "space_before_semicolon": true,
115
+ "result_name": "result",
116
+ "local_variable_pattern": "^[_a-z][A-Za-z0-9]*$",
117
+ "functions": {
118
+ "let": { "layout": "let", "multiline": "always" },
119
+ "while": { "layout": "while", "multiline": "always" },
120
+ "case": { "layout": "pairs" },
121
+ "if": { "multiline": "always" }
122
+ }
123
+ }
124
+ ```
125
+
126
+ Defaults implement the Thomas DS style guide with 4-space indent. An organisation's
127
+ pack is just this file plus their `guidelines.md`, versioned in their repo.
128
+
129
+ ### Presets
130
+
131
+ Named, ready-made style packs — pick one in the web app's toolbar or via
132
+ `fmstyle --preset <name>` (a `--config` file overrides preset keys;
133
+ `fmstyle presets` lists them):
134
+
135
+ | Preset | Source | Highlights |
136
+ |---|---|---|
137
+ | `oogi` | OOGI BV FileMaker Development Style Guide | 4-space indent, blank-line Let blocks, explicit `result`, leading-semicolon JSONSetElement (§8.1) |
138
+ | `soliant` | *draft* — derived from observed AMPS conventions | tab indent, compact Let blocks, `_camelCase` locals |
139
+
140
+ Presets must be **derived from a firm's real conventions, not invented** — the
141
+ Soliant preset is marked draft until someone at Soliant blesses it. Adding a
142
+ consultancy's preset is a PR with one dict in `fmstyle/presets.py` (+ the same
143
+ entry in the web app).
144
+
145
+ **Community edition (vision).** The end game is a `community` preset governed in
146
+ the open: proposals per rule ("blank lines in Let: yes/no"), public voting, a
147
+ versioned result (`community-2027.1`). If enough firms adopt it, FileMaker gets
148
+ what gofmt gave Go — one format, zero debates. The deterministic engine is the
149
+ prerequisite; the governance can start as GitHub issues + reactions before it
150
+ needs anything fancier.
151
+
152
+ **Per-function rules** — every FileMaker (or custom) function name can get its own
153
+ entry under `"functions"`, so how *each* call formats is a config edit, not a code
154
+ change:
155
+
156
+ | Option | Values | Meaning |
157
+ |---|---|---|
158
+ | `layout` | `args` (default) | one argument per line when exploded |
159
+ | | `pairs` | condition ; result pairs per line (Case-style) |
160
+ | | `let` / `while` | the mandatory Let / While block shapes |
161
+ | `multiline` | `auto` (default) | explode only when the call exceeds `width` |
162
+ | | `always` | always explode, even when it would fit |
163
+
164
+ (`force_multiline: [...]` is still accepted as a legacy shorthand for
165
+ `multiline: "always"`.)
166
+
167
+ ## Validated against real solutions
168
+
169
+ `tools/corpus_audit.py` replays **every calculation** from an
170
+ [fm-ddr-analyzer](../fm-ddr-analyzer) SQLite database through the formatter:
171
+
172
+ | Solution | Calculations | Format cleanly |
173
+ |---|---|---|
174
+ | tjouly (4-file DDR, 2026-05) | 47,665 | **94.8%** |
175
+ | AMPS / Sunsolar (9-file DDR, 2026-06) | 88,401 | **94.2%** |
176
+
177
+ The remainder is almost entirely DDR-export artifacts (several calcs
178
+ concatenated into one layout-object entry, `<Field Missing>` placeholders) which
179
+ the formatter correctly *refuses* rather than mangles. The audit drove real
180
+ grammar fixes: `~`/`#`/dotted/unicode names (`$$~DISABLETRIGGERS`,
181
+ `#ScriptResultJSON`, `Session.GetValue`, `Rep 5¢ Commission`), repetition
182
+ references (`field[11]`), `x = not y` assignments, trailing semicolons
183
+ (`Case ( a ; b ; )`), and commented-out calcs. It also inventories which
184
+ built-in and custom functions a solution actually uses (`--functions`) — input
185
+ for choosing per-function rules.
186
+
187
+ ## One tool, three fronts (convergence plan)
188
+
189
+ Three FileMaker tools now share one DNA (stream Claris XML, pure stdlib, private
190
+ by design):
191
+
192
+ | Tool | Input | Output |
193
+ |---|---|---|
194
+ | [fm-ddr-analyzer](../fm-ddr-analyzer) | DDR XML | queryable SQLite + explorer |
195
+ | [accessibility-label-patcher](../soliant/spikes/accessibility-label-patcher) | SaXML | FMUpgradeTool patch |
196
+ | **fm-style** (this) | calculation text (later: SaXML/snippets) | formatted code + lint report |
197
+
198
+ Convergence happens in two layers, without merging repos prematurely:
199
+
200
+ 1. **Shared cores.** The calculation tokenizer built here is directly reusable by
201
+ the DDR analyzer (pretty-printing calcs in the explorer, structural search) and
202
+ by any SaXML-processing step. Extract to a shared package only when a second
203
+ consumer actually lands.
204
+ 2. **One web umbrella.** Like the DDR analyzer's zero-upload web app, fm-style gets
205
+ a client-side page (paste calc → formatted, org pack loadable from a JSON file).
206
+ The umbrella site (oogi.io) presents them as one FileMaker toolbelt; each tool
207
+ stays independently shippable underneath.
208
+
209
+ ## Roadmap
210
+
211
+ - [x] **v0.1 — calculation formatter + first lint rules + CLI** (this).
212
+ - [x] **Web app** — single client-side HTML page (`fmstyle/web/index.html`), JS
213
+ port parity-tested byte-for-byte against Python. Paste → format → copy;
214
+ load your `fmstyle.json`; live lint findings; light/dark.
215
+ - [ ] **Copy as FM object** — where applicable, wrap output as `fmxmlsnippet`
216
+ (custom functions, Set Variable steps) so paste lands as a real object.
217
+ - [x] **LLM harness packaging** — a Claude Code skill (`fmstyle install-skill`)
218
+ that makes the assistant auto-run `fmstyle format` on FileMaker calcs it
219
+ writes, plus PyPI-ready packaging (wheel bundles the web app + skill).
220
+ Next: publish to PyPI; optionally inject the org's `guidelines.md`.
221
+ - [ ] **Script-step formatting & linting** — via SaXML (the accessibility patcher
222
+ already proves parse→patch round-trips): script naming, error-handling
223
+ blocks, comment headers from the style guide.
224
+ - [ ] **More lint rules** — custom function headers, `While` counter conventions,
225
+ magic-number detection, configurable per org.
226
+ - [ ] **Community preset + voting** — public per-rule proposals and voting
227
+ (GitHub issues/reactions to start), producing a versioned `community`
228
+ preset that firms can adopt — one unified FileMaker format, eventually.
229
+ - [ ] **DDR synergy** — lint a whole solution by piping every calculation from an
230
+ fm-ddr-analyzer SQLite DB through `fmstyle lint` (style report for an entire
231
+ file, like a health report).
232
+
233
+ ## Known limitations (v0.1)
234
+
235
+ - Calculation expressions only — no script steps yet (see roadmap).
236
+ - Reserved/ambiguous names: a bare multi-word field reference is accepted
237
+ verbatim; a field literally named like a keyword (`and`) needs `${and}`.
238
+ - Comments in unusual positions (e.g. between an operator and its operand) are
239
+ preserved but may be moved to the nearest line boundary; if preservation is
240
+ ever impossible the tool refuses rather than guesses.
241
+ - Function names are kept in the author's casing (no canonical-case rewrite yet).
242
+
243
+ ## Tech stack
244
+
245
+ | Concern | Choice | Why |
246
+ |---|---|---|
247
+ | Language | Python 3.10+, stdlib only | Zero install friction, matches fm-ddr-analyzer |
248
+ | Parsing | hand-written lexer + recursive descent | FM calc grammar is small; full control over verbatim tokens |
249
+ | Safety | token-stream equality check | formatter can provably never change semantics |
250
+ | Config | JSON dataclass | trivially shareable as an "org style pack" |
251
+
252
+ ## Project structure
253
+
254
+ ```
255
+ fm-style/
256
+ ├── README.md
257
+ ├── pyproject.toml
258
+ ├── fmstyle/
259
+ │ ├── web/index.html # zero-install client-side web app (JS engine + UI)
260
+ │ ├── __init__.py # format_calc / lint_calc API + safety check
261
+ │ ├── lexer.py # verbatim tokenizer (comments, spaced names, ${...})
262
+ │ ├── parser.py # recursive descent -> small AST
263
+ │ ├── printer.py # deterministic layout engine (Let/While/Case shapes)
264
+ │ ├── config.py # Style dataclass <- fmstyle.json
265
+ │ ├── rules.py # lint rules
266
+ │ └── cli.py # fmstyle format / lint
267
+ └── tests/
268
+ ├── test_fmstyle.py # exact-output, idempotence, safety, lint tests
269
+ ├── gen_parity_fixtures.py # Python reference -> parity_fixtures.json
270
+ ├── parity_fixtures.json # generated JS<->Python parity cases
271
+ └── test_parity.mjs # node: JS engine must match Python byte-for-byte
272
+ ```
@@ -0,0 +1,68 @@
1
+ """fmstyle - deterministic formatter + style engine for FileMaker calculations.
2
+
3
+ Public API:
4
+
5
+ format_calc(source, style) -> formatted text (token-preservation verified)
6
+ lint_calc(source, style) -> [(rule_id, message), ...]
7
+ Style -> configuration (an org's mechanical rules)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .config import Style
13
+ from .lexer import LexError, tokenize
14
+ from .parser import ParseError, Parser
15
+ from .printer import Printer
16
+ from .rules import lint as _lint
17
+
18
+ __version__ = "0.2.0"
19
+
20
+ __all__ = [
21
+ "format_calc",
22
+ "lint_calc",
23
+ "Style",
24
+ "LexError",
25
+ "ParseError",
26
+ "FormatSafetyError",
27
+ ]
28
+
29
+
30
+ class FormatSafetyError(RuntimeError):
31
+ """Formatting would have changed the token stream - refused."""
32
+
33
+
34
+ def _signature(source: str):
35
+ code = []
36
+ comments = []
37
+ for tok in tokenize(source):
38
+ comments.extend(c.text for c in tok.pre_comments)
39
+ if tok.kind != "EOF":
40
+ text = tok.text.lower() if tok.kind == "OP" else tok.text
41
+ code.append((tok.kind, text))
42
+ return code, comments
43
+
44
+
45
+ def format_calc(source: str, style: Style | None = None) -> str:
46
+ """Format a FileMaker calculation. Guaranteed semantics-preserving: the
47
+ output must re-tokenize to the exact same code tokens and comments as the
48
+ input, otherwise FormatSafetyError is raised and nothing is changed."""
49
+ style = style or Style()
50
+ tokens = tokenize(source)
51
+ if len(tokens) == 1: # comments only (e.g. a commented-out calc) - keep as is
52
+ return source
53
+ node = Parser(tokens).parse()
54
+ out = Printer(style).format(node)
55
+ if _signature(source) != _signature(out):
56
+ raise FormatSafetyError(
57
+ "formatting would alter the token stream; refusing (please report this input)"
58
+ )
59
+ return out
60
+
61
+
62
+ def lint_calc(source: str, style: Style | None = None):
63
+ """Run guideline lint rules over a calculation."""
64
+ style = style or Style()
65
+ tokens = tokenize(source)
66
+ if len(tokens) == 1: # comments only - nothing to lint
67
+ return []
68
+ return _lint(Parser(tokens).parse(), style)