fmddr 0.1.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.
- fmddr-0.1.0/.gitignore +25 -0
- fmddr-0.1.0/LICENSE +21 -0
- fmddr-0.1.0/PKG-INFO +324 -0
- fmddr-0.1.0/README.md +286 -0
- fmddr-0.1.0/docs/README.md +26 -0
- fmddr-0.1.0/pyproject.toml +76 -0
- fmddr-0.1.0/src/fmddr/__init__.py +2 -0
- fmddr-0.1.0/src/fmddr/cli.py +989 -0
- fmddr-0.1.0/src/fmddr/db.py +47 -0
- fmddr-0.1.0/src/fmddr/format/__init__.py +0 -0
- fmddr-0.1.0/src/fmddr/format/output.py +128 -0
- fmddr-0.1.0/src/fmddr/index/__init__.py +0 -0
- fmddr-0.1.0/src/fmddr/index/build.py +748 -0
- fmddr-0.1.0/src/fmddr/index/schema.sql +217 -0
- fmddr-0.1.0/src/fmddr/parser/__init__.py +0 -0
- fmddr-0.1.0/src/fmddr/parser/refs.py +48 -0
- fmddr-0.1.0/src/fmddr/parser/split.py +364 -0
- fmddr-0.1.0/src/fmddr/parser/stream.py +26 -0
- fmddr-0.1.0/src/fmddr/query/__init__.py +0 -0
- fmddr-0.1.0/src/fmddr/query/cf.py +79 -0
- fmddr-0.1.0/src/fmddr/query/fields.py +99 -0
- fmddr-0.1.0/src/fmddr/query/graph.py +93 -0
- fmddr-0.1.0/src/fmddr/query/impact.py +232 -0
- fmddr-0.1.0/src/fmddr/query/layouts.py +61 -0
- fmddr-0.1.0/src/fmddr/query/resolvers.py +128 -0
- fmddr-0.1.0/src/fmddr/query/scripts.py +244 -0
- fmddr-0.1.0/src/fmddr/query/search.py +41 -0
- fmddr-0.1.0/src/fmddr/query/tables.py +49 -0
- fmddr-0.1.0/tests/fixtures/mini.xml +163 -0
- fmddr-0.1.0/tests/test_demo_questions.py +340 -0
- fmddr-0.1.0/tests/test_smoke.py +212 -0
fmddr-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
*.pyo
|
|
4
|
+
.venv/
|
|
5
|
+
venv/
|
|
6
|
+
.env
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
*.fmddr.db
|
|
14
|
+
*.fmddr.db-journal
|
|
15
|
+
xmls-split/
|
|
16
|
+
|
|
17
|
+
# Build artifacts (generated; don't commit)
|
|
18
|
+
dist/
|
|
19
|
+
|
|
20
|
+
# Docs site
|
|
21
|
+
docs/node_modules/
|
|
22
|
+
docs/.astro/
|
|
23
|
+
docs/dist/
|
|
24
|
+
docs/.DS_Store
|
|
25
|
+
|
fmddr-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Corsi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fmddr-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fmddr
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A CLI and library for analyzing FileMaker DDRs. Built for agents — JSON-first, scriptable, headless.
|
|
5
|
+
Project-URL: Homepage, https://github.com/proofgeist/fmddr
|
|
6
|
+
Project-URL: Repository, https://github.com/proofgeist/fmddr
|
|
7
|
+
Project-URL: Issues, https://github.com/proofgeist/fmddr/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/proofgeist/fmddr#readme
|
|
9
|
+
Author-email: Chris Corsi <chris.corsi@proofgeist.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,database-design-report,ddr,filemaker,fmp,fmp12,fmperception,llm,refactoring,static-analysis
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Database
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
26
|
+
Classifier: Topic :: Utilities
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Requires-Dist: rich>=13.0
|
|
29
|
+
Requires-Dist: typer>=0.12
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
34
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
35
|
+
Provides-Extra: lxml
|
|
36
|
+
Requires-Dist: lxml>=5.0; extra == 'lxml'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# fmddr
|
|
40
|
+
|
|
41
|
+
A CLI and library for analyzing FileMaker DDRs (Database Design Reports).
|
|
42
|
+
|
|
43
|
+
`fmddr` parses a DDR XML once into a SQLite index, then answers structural and
|
|
44
|
+
cross-reference queries in milliseconds. The output is JSON-first and stable,
|
|
45
|
+
so an agent — or a script, or a developer at the terminal — can drive analysis
|
|
46
|
+
at the same depth a GUI tool offers, without the GUI.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
Requires Python 3.11+.
|
|
51
|
+
|
|
52
|
+
### From a built wheel (recommended for sharing)
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
# In this repo:
|
|
56
|
+
pip install build
|
|
57
|
+
python -m build
|
|
58
|
+
# → dist/fmddr-0.1.0-py3-none-any.whl (~42 KB, includes schema.sql)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Hand the wheel to anyone with Python 3.11+:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
pip install ./fmddr-0.1.0-py3-none-any.whl
|
|
65
|
+
# or for an isolated CLI install:
|
|
66
|
+
pipx install ./fmddr-0.1.0-py3-none-any.whl
|
|
67
|
+
|
|
68
|
+
fmddr --version
|
|
69
|
+
# fmddr 0.1.0
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### From source (for development)
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
pip install -e .
|
|
76
|
+
pip install -e .[dev] # adds pytest, ruff
|
|
77
|
+
pip install -e .[lxml] # adds libxml2-bound parser (optional)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Quickstart
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
# 1. Explode the DDR into a per-object XML tree (grep/diff friendly).
|
|
84
|
+
fmddr split path/to/Profile.xml --out ./xmls-split
|
|
85
|
+
|
|
86
|
+
# 2. Build the SQLite index (the "ingest" step).
|
|
87
|
+
fmddr index path/to/Profile.xml --out ./profile.fmddr.db
|
|
88
|
+
|
|
89
|
+
# 3. Query.
|
|
90
|
+
fmddr stats --db ./profile.fmddr.db
|
|
91
|
+
fmddr table fields "Production Run" -o table
|
|
92
|
+
fmddr field script-refs "Production Run::ProductionRunStatus"
|
|
93
|
+
fmddr script chain "Save Case Request" --depth 2 --format mermaid
|
|
94
|
+
fmddr cf called-by "FindWordPartsInText"
|
|
95
|
+
fmddr impact field "Customer::__UID"
|
|
96
|
+
fmddr unused --kind script
|
|
97
|
+
fmddr search "Customer*" --kind script
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
When stdout is a TTY, results render as Rich tables. Pipe to a file or another
|
|
101
|
+
process and you get JSON. Override with `-o {json,jsonl,table,markdown,csv,tsv}`.
|
|
102
|
+
|
|
103
|
+
## Why
|
|
104
|
+
|
|
105
|
+
A FileMaker file with thousands of fields, scripts, and layouts is hard to
|
|
106
|
+
refactor safely. Every change requires structural reasoning:
|
|
107
|
+
|
|
108
|
+
- *What scripts touch this field, and at which step?*
|
|
109
|
+
- *Which layouts show it, and which objects on those layouts?*
|
|
110
|
+
- *What does this script call, and who calls it?*
|
|
111
|
+
- *If this field is removed, what breaks?*
|
|
112
|
+
- *Where is this custom function used?*
|
|
113
|
+
- *Which steps run risky `ExecuteSQL` or `Evaluate`?*
|
|
114
|
+
- *What's dead code? What's an orphan reference?*
|
|
115
|
+
|
|
116
|
+
`fmddr` answers each of these in a one-line shell command with structured
|
|
117
|
+
output, so an agent can run thousands of these queries during a refactor —
|
|
118
|
+
programmatically, with stable output an LLM can parse.
|
|
119
|
+
|
|
120
|
+
## Performance
|
|
121
|
+
|
|
122
|
+
On a 491 MB DDR from a long-running production file:
|
|
123
|
+
|
|
124
|
+
- **Index build: ~7 s** (one-time, streams through the XML)
|
|
125
|
+
- 257 tables / 10,403 fields / 1,877 scripts / 189,026 steps
|
|
126
|
+
- 873 layouts / 70,564 layout refs / 3,083 TOs / 2,758 relationships
|
|
127
|
+
- 123 custom functions / 503 value lists / 8,810 predicate refs
|
|
128
|
+
- Queries return in **<50 ms**
|
|
129
|
+
|
|
130
|
+
Streaming `iterparse` + element-clearing keeps memory bounded regardless of DDR
|
|
131
|
+
size. SQLite with `journal_mode=MEMORY, synchronous=OFF` during build, plus
|
|
132
|
+
`VACUUM + PRAGMA optimize` at end.
|
|
133
|
+
|
|
134
|
+
## Command reference
|
|
135
|
+
|
|
136
|
+
### Ingest
|
|
137
|
+
|
|
138
|
+
| Command | Purpose |
|
|
139
|
+
| -------------------------------- | -------------------------------------------------- |
|
|
140
|
+
| `fmddr split <ddr>` | Per-object XML tree (grep/diff friendly) |
|
|
141
|
+
| `fmddr index <ddr> --out <db>` | Build the SQLite index |
|
|
142
|
+
| `fmddr stats` | Counts and metadata recorded in the index |
|
|
143
|
+
| `fmddr open` | Interactive `sqlite3` shell against the index |
|
|
144
|
+
|
|
145
|
+
### Tables and fields
|
|
146
|
+
|
|
147
|
+
| Command | Purpose |
|
|
148
|
+
| ----------------------------------------------- | -------------------------------------------------------- |
|
|
149
|
+
| `fmddr table list` | All base tables with field counts |
|
|
150
|
+
| `fmddr table show <name\|id>` | Table metadata (records, calc/unstored counts) |
|
|
151
|
+
| `fmddr table fields <name\|id> [--type ...] [--unstored]` | Fields on a table, optionally filtered |
|
|
152
|
+
| `fmddr field show <Table::Field\|id>` | Field metadata (calc text, AutoEnter, storage flags) |
|
|
153
|
+
| `fmddr field references <token> [--kind ...]` | All references, denormalized |
|
|
154
|
+
| `fmddr field script-refs <token>` | One row per (script, step) that names the field |
|
|
155
|
+
| `fmddr field on-layouts <token>` | Distinct layouts that show the field |
|
|
156
|
+
| `fmddr field field-refs <token>` | Other fields whose calcs name this field |
|
|
157
|
+
| `fmddr field where <token>` | Counts grouped by `owner_kind` |
|
|
158
|
+
|
|
159
|
+
### Scripts and steps
|
|
160
|
+
|
|
161
|
+
| Command | Purpose |
|
|
162
|
+
| ------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
163
|
+
| `fmddr script list [--folder ...] [--name ...]` | All scripts, optionally filtered |
|
|
164
|
+
| `fmddr script show <token>` | Script header + step count |
|
|
165
|
+
| `fmddr script steps <token> [--type ...] [--has ...]` | Steps; `--has execute_sql\|evaluate\|get_script_parameter\|get_script_result` |
|
|
166
|
+
| `fmddr script calls <token>` | Scripts called from this script |
|
|
167
|
+
| `fmddr script called-by <token>` | Everything that calls this script |
|
|
168
|
+
| `fmddr script fields <token>` | Distinct fields touched anywhere in this script |
|
|
169
|
+
| `fmddr script elements <token>` | Every distinct (field, script, layout, CF) referenced |
|
|
170
|
+
| `fmddr script calcs <token>` | Every step's text |
|
|
171
|
+
| `fmddr script chain <token> --depth N --format mermaid\|dot` | BFS call-chain diagram (Mermaid / Graphviz / JSON) |
|
|
172
|
+
| `fmddr script grep <token> <regex>` | Regex search step text within a script |
|
|
173
|
+
| `fmddr step show "<Script>:<index>"` | Full step detail (FMP step id, has\_\* flags, var name, text) |
|
|
174
|
+
| `fmddr step refs "<Script>:<index>"` | All fields/scripts/layouts/CFs this single step references |
|
|
175
|
+
| `fmddr risky-steps --has <flag> [--limit N]` | Cross-script audit of risky steps (`execute_sql` etc.) |
|
|
176
|
+
|
|
177
|
+
### Layouts
|
|
178
|
+
|
|
179
|
+
| Command | Purpose |
|
|
180
|
+
| --------------------------------------------- | ------------------------------------------------------------- |
|
|
181
|
+
| `fmddr layout list [--folder ...]` | All layouts |
|
|
182
|
+
| `fmddr layout show <token>` | Layout header |
|
|
183
|
+
| `fmddr layout objects <token> [--type ...]` | Objects on the layout (Field/Button/Portal/TabControl/...) |
|
|
184
|
+
| `fmddr layout fields <token>` | Distinct fields shown on the layout |
|
|
185
|
+
| `fmddr layout scripts <token>` | Scripts referenced (buttons + ScriptTriggers) |
|
|
186
|
+
|
|
187
|
+
### Custom functions
|
|
188
|
+
|
|
189
|
+
| Command | Purpose |
|
|
190
|
+
| -------------------------------- | ---------------------------------------------------------------------- |
|
|
191
|
+
| `fmddr cf list` | All custom functions |
|
|
192
|
+
| `fmddr cf show <token>` | CF metadata + body |
|
|
193
|
+
| `fmddr cf calls <token>` | Other CFs this CF calls |
|
|
194
|
+
| `fmddr cf fields <token>` | Fields referenced inside this CF body |
|
|
195
|
+
| `fmddr cf called-by <token>` | Everywhere this CF is used |
|
|
196
|
+
|
|
197
|
+
### Relationship graph
|
|
198
|
+
|
|
199
|
+
| Command | Purpose |
|
|
200
|
+
| ------------------------------------------------------ | ------------------------------------------------ |
|
|
201
|
+
| `fmddr graph relationships` | All relationships |
|
|
202
|
+
| `fmddr graph tos [--base-table ...]` | Table occurrences, optionally filtered |
|
|
203
|
+
| `fmddr graph predicates <relationship_id>` | Join predicates for a relationship |
|
|
204
|
+
| `fmddr graph path <from-TO> <to-TO> [--max-hops N]` | Shortest TO→TO path through relationships |
|
|
205
|
+
|
|
206
|
+
### Higher-order
|
|
207
|
+
|
|
208
|
+
| Command | Purpose |
|
|
209
|
+
| ------------------------------------------------------------- | ------------------------------------------------------- |
|
|
210
|
+
| `fmddr impact field <token>` | What breaks if this field is removed |
|
|
211
|
+
| `fmddr impact script <token> [--depth N]` | Transitive script callers |
|
|
212
|
+
| `fmddr unused --kind <field\|script\|layout\|custom_function\|value_list\|table_occurrence>` | Dead code candidates |
|
|
213
|
+
| `fmddr orphans` | Refs whose target rows don't exist |
|
|
214
|
+
| `fmddr cleanup-candidates` | Names matching debt patterns (BACKUP/COPY/OLD/TEMP/...) |
|
|
215
|
+
|
|
216
|
+
### Search
|
|
217
|
+
|
|
218
|
+
| Command | Purpose |
|
|
219
|
+
| -------------------------------------------------- | --------------------------------------------------------- |
|
|
220
|
+
| `fmddr search <fts5-query> [--kind ...] [--limit N]` | FTS5 over names of every kind |
|
|
221
|
+
|
|
222
|
+
### Global flags
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
--db <path> Override default DB location (also $FMDDR_DB)
|
|
226
|
+
-o, --output {json,jsonl,table,markdown,csv,tsv} Force output format
|
|
227
|
+
--version Show version and exit
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
DB lookup order: `--db` flag → `$FMDDR_DB` → unique `*.fmddr.db` in CWD → error.
|
|
231
|
+
|
|
232
|
+
### Exit codes
|
|
233
|
+
|
|
234
|
+
| Code | Meaning |
|
|
235
|
+
| ---- | --------------------------------------------------- |
|
|
236
|
+
| 0 | OK |
|
|
237
|
+
| 1 | Not found (no field/script/layout matched) |
|
|
238
|
+
| 2 | Ambiguous (qualify with `Table::Field` or `--by-id`)|
|
|
239
|
+
| 64 | Bad usage |
|
|
240
|
+
| 70 | Internal error |
|
|
241
|
+
|
|
242
|
+
## Output envelope
|
|
243
|
+
|
|
244
|
+
Every command emits a stable JSON shape:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"kind": "field.script-refs",
|
|
249
|
+
"version": 1,
|
|
250
|
+
"ddr": {"path": "...", "sha256": "...", "indexed_at": "..."},
|
|
251
|
+
"query": {"field": {"id": 8712, "name": "ProductionRunStatus", "table": "Production Run"}},
|
|
252
|
+
"result": [
|
|
253
|
+
{
|
|
254
|
+
"script_id": 1245, "script_name": "Save Case Request", "folder": "case-management",
|
|
255
|
+
"step_index": 82, "fm_line": 83, "step_type_name": "Perform Script",
|
|
256
|
+
"step_text": "Perform Script [ \"Send Alert Notification\" ... ]",
|
|
257
|
+
"via_to_id": 207, "via_to_name": "Production Run"
|
|
258
|
+
}
|
|
259
|
+
],
|
|
260
|
+
"result_count": 1,
|
|
261
|
+
"truncated": false
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`kind` is the agent contract — agents can dispatch on it without parsing free
|
|
266
|
+
text. `version: 1` allows future schema bumps without breaking older callers.
|
|
267
|
+
|
|
268
|
+
## Tests double as runnable examples
|
|
269
|
+
|
|
270
|
+
`tests/test_demo_questions.py` runs through 22 representative analyses against
|
|
271
|
+
a synthetic mini DDR fixture. Run it with `-s` to see the formatted output for
|
|
272
|
+
each one:
|
|
273
|
+
|
|
274
|
+
```sh
|
|
275
|
+
pytest tests/test_demo_questions.py -s
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
When this file passes, the library answers all of them.
|
|
279
|
+
|
|
280
|
+
## Architecture
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
src/fmddr/
|
|
284
|
+
├── cli.py # Typer entry point (thin shell)
|
|
285
|
+
├── parser/
|
|
286
|
+
│ ├── stream.py # UTF-16 → UTF-8 decode + iterparse driver
|
|
287
|
+
│ ├── split.py # Per-object XML tree explosion
|
|
288
|
+
│ └── refs.py # The uniform Chunk[FieldRef|CustomFunctionRef] walker
|
|
289
|
+
├── index/
|
|
290
|
+
│ ├── schema.sql # Canonical SQLite DDL
|
|
291
|
+
│ └── build.py # Streaming insert with executemany batches + staged ref resolution
|
|
292
|
+
├── query/
|
|
293
|
+
│ ├── tables.py # base_table + field queries
|
|
294
|
+
│ ├── fields.py # Field reference drills
|
|
295
|
+
│ ├── scripts.py # script + step + steps/refs/calls/elements/calcs
|
|
296
|
+
│ ├── layouts.py # layouts + objects + fields + scripts
|
|
297
|
+
│ ├── cf.py # Custom function queries
|
|
298
|
+
│ ├── graph.py # TOs + relationships + predicates + path
|
|
299
|
+
│ ├── impact.py # impact / unused / orphans / script-chain
|
|
300
|
+
│ ├── search.py # FTS5 + script grep
|
|
301
|
+
│ └── resolvers.py # name|id → (kind, id) with ambiguity handling
|
|
302
|
+
├── format/
|
|
303
|
+
│ └── output.py # Stable envelope + multi-format rendering
|
|
304
|
+
└── db.py # Connection helpers + meta envelope info
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The reference graph is the heart: `field_ref`, `script_ref`, `layout_ref`,
|
|
308
|
+
`custom_function_ref`, `value_list_ref`, `table_ref` — every "where used /
|
|
309
|
+
depends on" question reduces to a one- or two-hop SQL query.
|
|
310
|
+
|
|
311
|
+
## Docs site
|
|
312
|
+
|
|
313
|
+
A full Astro/Starlight docs site lives in [`docs/`](./docs):
|
|
314
|
+
|
|
315
|
+
```sh
|
|
316
|
+
cd docs
|
|
317
|
+
npm install
|
|
318
|
+
npm run dev # http://localhost:4321
|
|
319
|
+
npm run build # static site → docs/dist/, deployable anywhere
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## License
|
|
323
|
+
|
|
324
|
+
MIT
|
fmddr-0.1.0/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# fmddr
|
|
2
|
+
|
|
3
|
+
A CLI and library for analyzing FileMaker DDRs (Database Design Reports).
|
|
4
|
+
|
|
5
|
+
`fmddr` parses a DDR XML once into a SQLite index, then answers structural and
|
|
6
|
+
cross-reference queries in milliseconds. The output is JSON-first and stable,
|
|
7
|
+
so an agent — or a script, or a developer at the terminal — can drive analysis
|
|
8
|
+
at the same depth a GUI tool offers, without the GUI.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Requires Python 3.11+.
|
|
13
|
+
|
|
14
|
+
### From a built wheel (recommended for sharing)
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
# In this repo:
|
|
18
|
+
pip install build
|
|
19
|
+
python -m build
|
|
20
|
+
# → dist/fmddr-0.1.0-py3-none-any.whl (~42 KB, includes schema.sql)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Hand the wheel to anyone with Python 3.11+:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pip install ./fmddr-0.1.0-py3-none-any.whl
|
|
27
|
+
# or for an isolated CLI install:
|
|
28
|
+
pipx install ./fmddr-0.1.0-py3-none-any.whl
|
|
29
|
+
|
|
30
|
+
fmddr --version
|
|
31
|
+
# fmddr 0.1.0
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### From source (for development)
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install -e .
|
|
38
|
+
pip install -e .[dev] # adds pytest, ruff
|
|
39
|
+
pip install -e .[lxml] # adds libxml2-bound parser (optional)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
# 1. Explode the DDR into a per-object XML tree (grep/diff friendly).
|
|
46
|
+
fmddr split path/to/Profile.xml --out ./xmls-split
|
|
47
|
+
|
|
48
|
+
# 2. Build the SQLite index (the "ingest" step).
|
|
49
|
+
fmddr index path/to/Profile.xml --out ./profile.fmddr.db
|
|
50
|
+
|
|
51
|
+
# 3. Query.
|
|
52
|
+
fmddr stats --db ./profile.fmddr.db
|
|
53
|
+
fmddr table fields "Production Run" -o table
|
|
54
|
+
fmddr field script-refs "Production Run::ProductionRunStatus"
|
|
55
|
+
fmddr script chain "Save Case Request" --depth 2 --format mermaid
|
|
56
|
+
fmddr cf called-by "FindWordPartsInText"
|
|
57
|
+
fmddr impact field "Customer::__UID"
|
|
58
|
+
fmddr unused --kind script
|
|
59
|
+
fmddr search "Customer*" --kind script
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
When stdout is a TTY, results render as Rich tables. Pipe to a file or another
|
|
63
|
+
process and you get JSON. Override with `-o {json,jsonl,table,markdown,csv,tsv}`.
|
|
64
|
+
|
|
65
|
+
## Why
|
|
66
|
+
|
|
67
|
+
A FileMaker file with thousands of fields, scripts, and layouts is hard to
|
|
68
|
+
refactor safely. Every change requires structural reasoning:
|
|
69
|
+
|
|
70
|
+
- *What scripts touch this field, and at which step?*
|
|
71
|
+
- *Which layouts show it, and which objects on those layouts?*
|
|
72
|
+
- *What does this script call, and who calls it?*
|
|
73
|
+
- *If this field is removed, what breaks?*
|
|
74
|
+
- *Where is this custom function used?*
|
|
75
|
+
- *Which steps run risky `ExecuteSQL` or `Evaluate`?*
|
|
76
|
+
- *What's dead code? What's an orphan reference?*
|
|
77
|
+
|
|
78
|
+
`fmddr` answers each of these in a one-line shell command with structured
|
|
79
|
+
output, so an agent can run thousands of these queries during a refactor —
|
|
80
|
+
programmatically, with stable output an LLM can parse.
|
|
81
|
+
|
|
82
|
+
## Performance
|
|
83
|
+
|
|
84
|
+
On a 491 MB DDR from a long-running production file:
|
|
85
|
+
|
|
86
|
+
- **Index build: ~7 s** (one-time, streams through the XML)
|
|
87
|
+
- 257 tables / 10,403 fields / 1,877 scripts / 189,026 steps
|
|
88
|
+
- 873 layouts / 70,564 layout refs / 3,083 TOs / 2,758 relationships
|
|
89
|
+
- 123 custom functions / 503 value lists / 8,810 predicate refs
|
|
90
|
+
- Queries return in **<50 ms**
|
|
91
|
+
|
|
92
|
+
Streaming `iterparse` + element-clearing keeps memory bounded regardless of DDR
|
|
93
|
+
size. SQLite with `journal_mode=MEMORY, synchronous=OFF` during build, plus
|
|
94
|
+
`VACUUM + PRAGMA optimize` at end.
|
|
95
|
+
|
|
96
|
+
## Command reference
|
|
97
|
+
|
|
98
|
+
### Ingest
|
|
99
|
+
|
|
100
|
+
| Command | Purpose |
|
|
101
|
+
| -------------------------------- | -------------------------------------------------- |
|
|
102
|
+
| `fmddr split <ddr>` | Per-object XML tree (grep/diff friendly) |
|
|
103
|
+
| `fmddr index <ddr> --out <db>` | Build the SQLite index |
|
|
104
|
+
| `fmddr stats` | Counts and metadata recorded in the index |
|
|
105
|
+
| `fmddr open` | Interactive `sqlite3` shell against the index |
|
|
106
|
+
|
|
107
|
+
### Tables and fields
|
|
108
|
+
|
|
109
|
+
| Command | Purpose |
|
|
110
|
+
| ----------------------------------------------- | -------------------------------------------------------- |
|
|
111
|
+
| `fmddr table list` | All base tables with field counts |
|
|
112
|
+
| `fmddr table show <name\|id>` | Table metadata (records, calc/unstored counts) |
|
|
113
|
+
| `fmddr table fields <name\|id> [--type ...] [--unstored]` | Fields on a table, optionally filtered |
|
|
114
|
+
| `fmddr field show <Table::Field\|id>` | Field metadata (calc text, AutoEnter, storage flags) |
|
|
115
|
+
| `fmddr field references <token> [--kind ...]` | All references, denormalized |
|
|
116
|
+
| `fmddr field script-refs <token>` | One row per (script, step) that names the field |
|
|
117
|
+
| `fmddr field on-layouts <token>` | Distinct layouts that show the field |
|
|
118
|
+
| `fmddr field field-refs <token>` | Other fields whose calcs name this field |
|
|
119
|
+
| `fmddr field where <token>` | Counts grouped by `owner_kind` |
|
|
120
|
+
|
|
121
|
+
### Scripts and steps
|
|
122
|
+
|
|
123
|
+
| Command | Purpose |
|
|
124
|
+
| ------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
125
|
+
| `fmddr script list [--folder ...] [--name ...]` | All scripts, optionally filtered |
|
|
126
|
+
| `fmddr script show <token>` | Script header + step count |
|
|
127
|
+
| `fmddr script steps <token> [--type ...] [--has ...]` | Steps; `--has execute_sql\|evaluate\|get_script_parameter\|get_script_result` |
|
|
128
|
+
| `fmddr script calls <token>` | Scripts called from this script |
|
|
129
|
+
| `fmddr script called-by <token>` | Everything that calls this script |
|
|
130
|
+
| `fmddr script fields <token>` | Distinct fields touched anywhere in this script |
|
|
131
|
+
| `fmddr script elements <token>` | Every distinct (field, script, layout, CF) referenced |
|
|
132
|
+
| `fmddr script calcs <token>` | Every step's text |
|
|
133
|
+
| `fmddr script chain <token> --depth N --format mermaid\|dot` | BFS call-chain diagram (Mermaid / Graphviz / JSON) |
|
|
134
|
+
| `fmddr script grep <token> <regex>` | Regex search step text within a script |
|
|
135
|
+
| `fmddr step show "<Script>:<index>"` | Full step detail (FMP step id, has\_\* flags, var name, text) |
|
|
136
|
+
| `fmddr step refs "<Script>:<index>"` | All fields/scripts/layouts/CFs this single step references |
|
|
137
|
+
| `fmddr risky-steps --has <flag> [--limit N]` | Cross-script audit of risky steps (`execute_sql` etc.) |
|
|
138
|
+
|
|
139
|
+
### Layouts
|
|
140
|
+
|
|
141
|
+
| Command | Purpose |
|
|
142
|
+
| --------------------------------------------- | ------------------------------------------------------------- |
|
|
143
|
+
| `fmddr layout list [--folder ...]` | All layouts |
|
|
144
|
+
| `fmddr layout show <token>` | Layout header |
|
|
145
|
+
| `fmddr layout objects <token> [--type ...]` | Objects on the layout (Field/Button/Portal/TabControl/...) |
|
|
146
|
+
| `fmddr layout fields <token>` | Distinct fields shown on the layout |
|
|
147
|
+
| `fmddr layout scripts <token>` | Scripts referenced (buttons + ScriptTriggers) |
|
|
148
|
+
|
|
149
|
+
### Custom functions
|
|
150
|
+
|
|
151
|
+
| Command | Purpose |
|
|
152
|
+
| -------------------------------- | ---------------------------------------------------------------------- |
|
|
153
|
+
| `fmddr cf list` | All custom functions |
|
|
154
|
+
| `fmddr cf show <token>` | CF metadata + body |
|
|
155
|
+
| `fmddr cf calls <token>` | Other CFs this CF calls |
|
|
156
|
+
| `fmddr cf fields <token>` | Fields referenced inside this CF body |
|
|
157
|
+
| `fmddr cf called-by <token>` | Everywhere this CF is used |
|
|
158
|
+
|
|
159
|
+
### Relationship graph
|
|
160
|
+
|
|
161
|
+
| Command | Purpose |
|
|
162
|
+
| ------------------------------------------------------ | ------------------------------------------------ |
|
|
163
|
+
| `fmddr graph relationships` | All relationships |
|
|
164
|
+
| `fmddr graph tos [--base-table ...]` | Table occurrences, optionally filtered |
|
|
165
|
+
| `fmddr graph predicates <relationship_id>` | Join predicates for a relationship |
|
|
166
|
+
| `fmddr graph path <from-TO> <to-TO> [--max-hops N]` | Shortest TO→TO path through relationships |
|
|
167
|
+
|
|
168
|
+
### Higher-order
|
|
169
|
+
|
|
170
|
+
| Command | Purpose |
|
|
171
|
+
| ------------------------------------------------------------- | ------------------------------------------------------- |
|
|
172
|
+
| `fmddr impact field <token>` | What breaks if this field is removed |
|
|
173
|
+
| `fmddr impact script <token> [--depth N]` | Transitive script callers |
|
|
174
|
+
| `fmddr unused --kind <field\|script\|layout\|custom_function\|value_list\|table_occurrence>` | Dead code candidates |
|
|
175
|
+
| `fmddr orphans` | Refs whose target rows don't exist |
|
|
176
|
+
| `fmddr cleanup-candidates` | Names matching debt patterns (BACKUP/COPY/OLD/TEMP/...) |
|
|
177
|
+
|
|
178
|
+
### Search
|
|
179
|
+
|
|
180
|
+
| Command | Purpose |
|
|
181
|
+
| -------------------------------------------------- | --------------------------------------------------------- |
|
|
182
|
+
| `fmddr search <fts5-query> [--kind ...] [--limit N]` | FTS5 over names of every kind |
|
|
183
|
+
|
|
184
|
+
### Global flags
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
--db <path> Override default DB location (also $FMDDR_DB)
|
|
188
|
+
-o, --output {json,jsonl,table,markdown,csv,tsv} Force output format
|
|
189
|
+
--version Show version and exit
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
DB lookup order: `--db` flag → `$FMDDR_DB` → unique `*.fmddr.db` in CWD → error.
|
|
193
|
+
|
|
194
|
+
### Exit codes
|
|
195
|
+
|
|
196
|
+
| Code | Meaning |
|
|
197
|
+
| ---- | --------------------------------------------------- |
|
|
198
|
+
| 0 | OK |
|
|
199
|
+
| 1 | Not found (no field/script/layout matched) |
|
|
200
|
+
| 2 | Ambiguous (qualify with `Table::Field` or `--by-id`)|
|
|
201
|
+
| 64 | Bad usage |
|
|
202
|
+
| 70 | Internal error |
|
|
203
|
+
|
|
204
|
+
## Output envelope
|
|
205
|
+
|
|
206
|
+
Every command emits a stable JSON shape:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"kind": "field.script-refs",
|
|
211
|
+
"version": 1,
|
|
212
|
+
"ddr": {"path": "...", "sha256": "...", "indexed_at": "..."},
|
|
213
|
+
"query": {"field": {"id": 8712, "name": "ProductionRunStatus", "table": "Production Run"}},
|
|
214
|
+
"result": [
|
|
215
|
+
{
|
|
216
|
+
"script_id": 1245, "script_name": "Save Case Request", "folder": "case-management",
|
|
217
|
+
"step_index": 82, "fm_line": 83, "step_type_name": "Perform Script",
|
|
218
|
+
"step_text": "Perform Script [ \"Send Alert Notification\" ... ]",
|
|
219
|
+
"via_to_id": 207, "via_to_name": "Production Run"
|
|
220
|
+
}
|
|
221
|
+
],
|
|
222
|
+
"result_count": 1,
|
|
223
|
+
"truncated": false
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`kind` is the agent contract — agents can dispatch on it without parsing free
|
|
228
|
+
text. `version: 1` allows future schema bumps without breaking older callers.
|
|
229
|
+
|
|
230
|
+
## Tests double as runnable examples
|
|
231
|
+
|
|
232
|
+
`tests/test_demo_questions.py` runs through 22 representative analyses against
|
|
233
|
+
a synthetic mini DDR fixture. Run it with `-s` to see the formatted output for
|
|
234
|
+
each one:
|
|
235
|
+
|
|
236
|
+
```sh
|
|
237
|
+
pytest tests/test_demo_questions.py -s
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
When this file passes, the library answers all of them.
|
|
241
|
+
|
|
242
|
+
## Architecture
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
src/fmddr/
|
|
246
|
+
├── cli.py # Typer entry point (thin shell)
|
|
247
|
+
├── parser/
|
|
248
|
+
│ ├── stream.py # UTF-16 → UTF-8 decode + iterparse driver
|
|
249
|
+
│ ├── split.py # Per-object XML tree explosion
|
|
250
|
+
│ └── refs.py # The uniform Chunk[FieldRef|CustomFunctionRef] walker
|
|
251
|
+
├── index/
|
|
252
|
+
│ ├── schema.sql # Canonical SQLite DDL
|
|
253
|
+
│ └── build.py # Streaming insert with executemany batches + staged ref resolution
|
|
254
|
+
├── query/
|
|
255
|
+
│ ├── tables.py # base_table + field queries
|
|
256
|
+
│ ├── fields.py # Field reference drills
|
|
257
|
+
│ ├── scripts.py # script + step + steps/refs/calls/elements/calcs
|
|
258
|
+
│ ├── layouts.py # layouts + objects + fields + scripts
|
|
259
|
+
│ ├── cf.py # Custom function queries
|
|
260
|
+
│ ├── graph.py # TOs + relationships + predicates + path
|
|
261
|
+
│ ├── impact.py # impact / unused / orphans / script-chain
|
|
262
|
+
│ ├── search.py # FTS5 + script grep
|
|
263
|
+
│ └── resolvers.py # name|id → (kind, id) with ambiguity handling
|
|
264
|
+
├── format/
|
|
265
|
+
│ └── output.py # Stable envelope + multi-format rendering
|
|
266
|
+
└── db.py # Connection helpers + meta envelope info
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The reference graph is the heart: `field_ref`, `script_ref`, `layout_ref`,
|
|
270
|
+
`custom_function_ref`, `value_list_ref`, `table_ref` — every "where used /
|
|
271
|
+
depends on" question reduces to a one- or two-hop SQL query.
|
|
272
|
+
|
|
273
|
+
## Docs site
|
|
274
|
+
|
|
275
|
+
A full Astro/Starlight docs site lives in [`docs/`](./docs):
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
cd docs
|
|
279
|
+
npm install
|
|
280
|
+
npm run dev # http://localhost:4321
|
|
281
|
+
npm run build # static site → docs/dist/, deployable anywhere
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
MIT
|