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 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