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 +283 -0
- fmstyle-0.2.0/README.md +272 -0
- fmstyle-0.2.0/fmstyle/__init__.py +68 -0
- fmstyle-0.2.0/fmstyle/cli.py +163 -0
- fmstyle-0.2.0/fmstyle/config.py +105 -0
- fmstyle-0.2.0/fmstyle/lexer.py +272 -0
- fmstyle-0.2.0/fmstyle/parser.py +343 -0
- fmstyle-0.2.0/fmstyle/presets.py +72 -0
- fmstyle-0.2.0/fmstyle/printer.py +304 -0
- fmstyle-0.2.0/fmstyle/rules.py +70 -0
- fmstyle-0.2.0/fmstyle/skill/SKILL.md +101 -0
- fmstyle-0.2.0/fmstyle/web/index.html +1338 -0
- fmstyle-0.2.0/fmstyle.egg-info/PKG-INFO +283 -0
- fmstyle-0.2.0/fmstyle.egg-info/SOURCES.txt +18 -0
- fmstyle-0.2.0/fmstyle.egg-info/dependency_links.txt +1 -0
- fmstyle-0.2.0/fmstyle.egg-info/entry_points.txt +2 -0
- fmstyle-0.2.0/fmstyle.egg-info/top_level.txt +1 -0
- fmstyle-0.2.0/pyproject.toml +28 -0
- fmstyle-0.2.0/setup.cfg +4 -0
- fmstyle-0.2.0/tests/test_fmstyle.py +300 -0
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
|
+
```
|
fmstyle-0.2.0/README.md
ADDED
|
@@ -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)
|