A CLI and library for analyzing FileMaker DDRs. Built for agents — JSON-first, scriptable, headless.
Project description
fmddr
A CLI and library for analyzing FileMaker DDRs (Database Design Reports).
fmddr parses a DDR XML once into a SQLite index, then answers structural and
cross-reference queries in milliseconds. The output is JSON-first and stable,
so an agent — or a script, or a developer at the terminal — can drive analysis
at the same depth a GUI tool offers, without the GUI.
Install
Requires Python 3.11+ and repo access.
# Isolated CLI install (recommended):
uv tool install git+ssh://git@github.com/proofsh/fmddr.git
# or:
pipx install git+ssh://git@github.com/proofsh/fmddr.git
fmddr --version
From source (for development)
git clone git@github.com:proofsh/fmddr.git
cd fmddr
pip install -e .[dev] # adds pytest, ruff, build
pip install -e .[lxml] # adds libxml2-bound parser (optional)
Building a wheel locally
pip install build
python -m build
# → dist/fmddr-<version>-py3-none-any.whl
Quickstart
# 1. Explode the DDR into a per-object XML tree (grep/diff friendly).
fmddr split path/to/Profile.xml --out ./xmls-split
# 2. Build the SQLite index (the "ingest" step).
# Works on a single DDR (`type="Report"`) or a `Summary.xml` that links
# multiple files together — both produce one queryable index.
fmddr index path/to/Profile.xml --out ./profile.fmddr.db
fmddr index path/to/Summary.xml --out ./solution.fmddr.db
# 3. Query.
fmddr stats --db ./profile.fmddr.db
fmddr table fields "Production Run" -o table
fmddr field script-refs "Production Run::ProductionRunStatus"
fmddr script chain "Save Case Request" --depth 2 --format mermaid
fmddr cf called-by "FindWordPartsInText"
fmddr impact field "Customer::__UID"
fmddr unused --kind script
fmddr search "Customer*" --kind script
When stdout is a TTY, results render as Rich tables. Pipe to a file or another
process and you get JSON. Override with -o {json,jsonl,table,markdown,csv,tsv}.
Why
A FileMaker file with thousands of fields, scripts, and layouts is hard to refactor safely. Every change requires structural reasoning:
- What scripts touch this field, and at which step?
- Which layouts show it, and which objects on those layouts?
- What does this script call, and who calls it?
- If this field is removed, what breaks?
- Where is this custom function used?
- Which steps run risky
ExecuteSQLorEvaluate? - What's dead code? What's an orphan reference?
fmddr answers each of these in a one-line shell command with structured
output, so an agent can run thousands of these queries during a refactor —
programmatically, with stable output an LLM can parse.
Performance
On a 491 MB DDR from a long-running production file:
- Index build: ~7 s (one-time, streams through the XML)
- 257 tables / 10,403 fields / 1,877 scripts / 189,026 steps
- 873 layouts / 70,564 layout refs / 3,083 TOs / 2,758 relationships
- 123 custom functions / 503 value lists / 8,810 predicate refs
- Queries return in <50 ms
Streaming iterparse + element-clearing keeps memory bounded regardless of DDR
size. SQLite with journal_mode=MEMORY, synchronous=OFF during build, plus
VACUUM + PRAGMA optimize at end.
Command reference
Ingest
| Command | Purpose |
|---|---|
fmddr split <ddr> |
Per-object file tree — scripts as .fmscript text (default; --script-format xml|both for DDR XML), everything else XML |
fmddr index <ddr> / <Summary.xml> |
Build the SQLite index (single file or multi-file) |
fmddr stats |
Counts and metadata recorded in the index |
fmddr file list |
Files ingested into this index, with per-file counts |
fmddr open |
Interactive sqlite3 shell against the index |
Tables and fields
| Command | Purpose |
|---|---|
fmddr table list |
All base tables with field counts |
fmddr table show <name|id> |
Table metadata (records, calc/unstored counts) |
fmddr table fields <name|id> [--type ...] [--unstored] |
Fields on a table, optionally filtered |
fmddr field show <Table::Field|id> |
Field metadata (calc text, AutoEnter, storage flags) |
fmddr field references <token> [--kind ...] |
All references, denormalized |
fmddr field script-refs <token> |
One row per (script, step) that names the field |
fmddr field on-layouts <token> |
Distinct layouts that show the field |
fmddr field field-refs <token> |
Other fields whose calcs name this field |
fmddr field where <token> |
Counts grouped by owner_kind |
Scripts and steps
| Command | Purpose |
|---|---|
fmddr script list [--folder ...] [--name ...] |
All scripts, optionally filtered |
fmddr script show <token> |
Script header + step count |
fmddr script steps <token> [--type ...] [--has ...] |
Steps; --has execute_sql|evaluate|get_script_parameter|get_script_result |
fmddr script calls <token> |
Scripts called from this script |
fmddr script called-by <token> |
Everything that calls this script |
fmddr script fields <token> |
Distinct fields touched anywhere in this script |
fmddr script elements <token> |
Every distinct (field, script, layout, CF) referenced |
fmddr script calcs <token> |
Every step's text |
fmddr script chain <token> --depth N --format mermaid|dot |
BFS call-chain diagram (Mermaid / Graphviz / JSON) |
fmddr script grep <token> <regex> |
Regex search step text within a script |
fmddr step show "<Script>:<index>" |
Full step detail (FMP step id, has_* flags, var name, text) |
fmddr step refs "<Script>:<index>" |
All fields/scripts/layouts/CFs this single step references |
fmddr risky-steps --has <flag> [--limit N] |
Cross-script audit of risky steps (execute_sql etc.) |
Layouts
| Command | Purpose |
|---|---|
fmddr layout list [--folder ...] |
All layouts |
fmddr layout show <token> |
Layout header |
fmddr layout objects <token> [--type ...] |
Objects on the layout (Field/Button/Portal/TabControl/...) |
fmddr layout fields <token> |
Distinct fields shown on the layout |
fmddr layout scripts <token> |
Scripts referenced (buttons + ScriptTriggers) |
Custom functions
| Command | Purpose |
|---|---|
fmddr cf list |
All custom functions |
fmddr cf show <token> |
CF metadata + body |
fmddr cf calls <token> |
Other CFs this CF calls |
fmddr cf fields <token> |
Fields referenced inside this CF body |
fmddr cf called-by <token> |
Everywhere this CF is used |
Relationship graph
| Command | Purpose |
|---|---|
fmddr graph relationships |
All relationships |
fmddr graph tos [--base-table ...] |
Table occurrences, optionally filtered |
fmddr graph predicates <relationship_id> |
Join predicates for a relationship |
fmddr graph path <from-TO> <to-TO> [--max-hops N] |
Shortest TO→TO path through relationships |
Higher-order
| Command | Purpose |
|---|---|
fmddr impact field <token> |
What breaks if this field is removed |
fmddr impact script <token> [--depth N] |
Transitive script callers |
fmddr unused --kind <field|script|layout|custom_function|value_list|table_occurrence> |
Dead code candidates |
fmddr orphans |
Refs whose target rows don't exist |
fmddr cleanup-candidates |
Names matching debt patterns (BACKUP/COPY/OLD/TEMP/...) |
Search
| Command | Purpose |
|---|---|
fmddr search <fts5-query> [--kind ...] [--limit N] |
FTS5 over names of every kind |
Clipboard (macOS only, v1)
Copy an object straight from the SQLite index (no split-tree required) and paste into FileMaker — or hand-edit a split-tree XML file first if you want the file-on-disk workflow.
| Command | Purpose |
|---|---|
fmddr copy --script "<name>" |
Look up the script in the index and copy as <fmxmlsnippet> |
fmddr copy --table "<name>" |
Same for a base table |
fmddr copy --cf "<name>" |
Same for a custom function (per-CF; split-tree only emits a whole catalog) |
fmddr copy --vl "<name>" |
Same for a value list (per-VL; split-tree only emits a whole catalog) |
fmddr copy --script "<name>" --steps all|5-12 |
Copy script steps (all or a subset) as a ScriptStep — paste into a script |
fmddr copy <split-xml-path> |
File-path mode: validate + wrap + push (split-tree workflow) |
fmddr paste [--type Script|Table|...] |
Read FM clipboard XML and emit <fmxmlsnippet> |
fmddr validate --script "<name>" (or --table, --cf, --vl) |
Run pre-clipboard guards against the indexed object |
fmddr validate <split-xml-path> |
File-path mode: guards against a split-tree file |
fmddr clipboard formats |
List FM-recognized clipboard formats currently present |
Index-backed mode requires an index built with --keep-xml (the
default since 0.9). Supports <Script>, <BaseTable>,
<CustomFunction>, and <ValueList> roots in v1. Per-<Layout>
copy is not implemented: FileMaker has no whole-Layout clipboard
format (only LayoutObject for individual objects within a layout).
See docs/guides/clipboard.
Compile (script text → clipboard)
Write FileMaker script steps as plain text — exactly as Script
Workspace displays them — and compile to a pasteable
<fmxmlsnippet>. Field, script, and layout names are resolved to
FileMaker internal ids via the index, so agents never hand-write ids
or per-step XML shapes.
$ cat fix.fmscript
Set Variable [ $ids ; Value: ExecuteSQL ( "SELECT id FROM x" ; "" ; "" ) ]
If [ ValueCount ( $ids ) > 0 ]
Set Field [ Customer::Status ; "active" ]
End If
$ fmddr compile fix.fmscript --copy # → clipboard, paste into Script Workspace
| Command | Purpose |
|---|---|
fmddr compile [file|-] |
Compile text (file or stdin) to a ScriptStep snippet |
fmddr compile … --script-name "<name>" |
Emit a whole-Script snippet (pasting creates a new script) |
fmddr compile … --copy |
Push the snippet to the macOS clipboard as a FileMaker object |
fmddr compile … --validate-only |
Parse + resolve + validate, no snippet |
fmddr compile … --out file.xml |
Write the snippet XML to a file |
fmddr compile … --no-index |
Skip name→id resolution (FM re-resolves by name on paste) |
A VS Code extension for .fmscript files lives in
editors/vscode/ — syntax highlighting generated
from the compiler's step registry, validation squiggles via
fmddr compile --validate-only, quick fixes from did-you-mean
suggestions, and a Compile-to-FileMaker-Clipboard command. Build with
cd editors/vscode && npm ci && npm run package.
Grammar notes: one step per line; multi-line calculations continue
until brackets balance; // prefix disables a step; # starts a
comment; indentation is ignored (block nesting derives from
If/Loop/End If sequence). 88 step types have a text grammar (≥99% of
real-world steps by frequency); the rest — steps whose display text
can't reconstruct their config, like Send Mail, Import/Export
Records, Print, or finds with saved requests — fail with
E_UNSUPPORTED_STEP and can be passed through verbatim with the
escape hatch: @xml <Step …>…</Step> (or a fenced @xml <<< … >>>
block). Errors are structured JSON with 1-based line numbers and
did-you-mean suggestions.
Global flags
--db <path> Override default DB location (also $FMDDR_DB)
-o, --output {json,jsonl,table,markdown,csv,tsv} Force output format
--version Show version and exit
DB lookup order: --db flag → $FMDDR_DB → unique *.fmddr.db in CWD → error.
Exit codes
| Code | Meaning |
|---|---|
| 0 | OK |
| 1 | Not found (no field/script/layout matched) |
| 2 | Ambiguous (qualify with Table::Field or --by-id) |
| 64 | Bad usage |
| 70 | Internal error |
Output envelope
Every command emits a stable JSON shape:
{
"kind": "field.script-refs",
"version": 1,
"ddr": {"path": "...", "sha256": "...", "indexed_at": "..."},
"query": {"field": {"id": 8712, "name": "ProductionRunStatus", "table": "Production Run"}},
"result": [
{
"script_id": 1245, "script_name": "Save Case Request", "folder": "case-management",
"step_index": 82, "fm_line": 83, "step_type_name": "Perform Script",
"step_text": "Perform Script [ \"Send Alert Notification\" ... ]",
"via_to_id": 207, "via_to_name": "Production Run"
}
],
"result_count": 1,
"truncated": false
}
kind is the agent contract — agents can dispatch on it without parsing free
text. version: 1 allows future schema bumps without breaking older callers.
Tests double as runnable examples
tests/test_demo_questions.py runs through 22 representative analyses against
a synthetic mini DDR fixture. Run it with -s to see the formatted output for
each one:
pytest tests/test_demo_questions.py -s
When this file passes, the library answers all of them.
Architecture
src/fmddr/
├── cli.py # Typer entry point (thin shell)
├── parser/
│ ├── stream.py # UTF-16 → UTF-8 decode + iterparse driver
│ ├── split.py # Per-object XML tree explosion
│ └── refs.py # The uniform Chunk[FieldRef|CustomFunctionRef] walker
├── index/
│ ├── schema.sql # Canonical SQLite DDL
│ └── build.py # Streaming insert with executemany batches + staged ref resolution
├── query/
│ ├── tables.py # base_table + field queries
│ ├── fields.py # Field reference drills
│ ├── scripts.py # script + step + steps/refs/calls/elements/calcs
│ ├── layouts.py # layouts + objects + fields + scripts
│ ├── cf.py # Custom function queries
│ ├── graph.py # TOs + relationships + predicates + path
│ ├── impact.py # impact / unused / orphans / script-chain
│ ├── search.py # FTS5 + script grep
│ └── resolvers.py # name|id → (kind, id) with ambiguity handling
├── format/
│ └── output.py # Stable envelope + multi-format rendering
└── db.py # Connection helpers + meta envelope info
The reference graph is the heart: field_ref, script_ref, layout_ref,
custom_function_ref, value_list_ref, table_ref — every "where used /
depends on" question reduces to a one- or two-hop SQL query.
Docs site
A full Astro/Starlight docs site lives in docs/:
cd docs
npm install
npm run dev # http://localhost:4321
npm run build # static site → docs/dist/, deployable anywhere
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fmddr-1.0.0b6.tar.gz.
File metadata
- Download URL: fmddr-1.0.0b6.tar.gz
- Upload date:
- Size: 390.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d84babe3584bf88abbdecb4598f3855e0f26f755fb6f11b6a491ae6942aa6bd4
|
|
| MD5 |
e77da0c5e62308a8dec32f73d95c54ee
|
|
| BLAKE2b-256 |
143760573acb2a00d37adfdda216a4487d97b323645d80bcc889f37a5c90722e
|
File details
Details for the file fmddr-1.0.0b6-py3-none-any.whl.
File metadata
- Download URL: fmddr-1.0.0b6-py3-none-any.whl
- Upload date:
- Size: 335.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ccb160d8c3c8afa062a1eedb9b765c9464a4d40b60419612b8dfd3f7a8897e0
|
|
| MD5 |
6bb5f244bd16a9b96a469f9f12055a4e
|
|
| BLAKE2b-256 |
fa1d53d366802579b10c3d9138624cca2478d3451d46557839926a54c45df471
|