Metadata-Version: 2.4
Name: fmddr
Version: 0.8.2
Summary: A CLI and library for analyzing FileMaker DDRs. Built for agents — JSON-first, scriptable, headless.
Project-URL: Homepage, https://github.com/proofsh/fmddr
Project-URL: Repository, https://github.com/proofsh/fmddr
Project-URL: Issues, https://github.com/proofsh/fmddr/issues
Project-URL: Documentation, https://github.com/proofsh/fmddr#readme
Author-email: Chris Corsi <chris.corsi@proofgeist.com>
License: MIT
License-File: LICENSE
Keywords: agent,database-design-report,ddr,filemaker,fmp,fmp12,fmperception,llm,refactoring,static-analysis
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Utilities
Requires-Python: >=3.11
Requires-Dist: fastapi>=0.110
Requires-Dist: rich>=13.0
Requires-Dist: typer>=0.12
Requires-Dist: uvicorn[standard]>=0.27
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Provides-Extra: lxml
Requires-Dist: lxml>=5.0; extra == 'lxml'
Description-Content-Type: text/markdown

# 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+.

```sh
pip install fmddr
# or for an isolated CLI install:
pipx install fmddr

fmddr --version
# fmddr 0.1.0
```

[Listed on PyPI →](https://pypi.org/project/fmddr/)

### From source (for development)

```sh
git clone https://github.com/proofsh/fmddr.git
cd fmddr
pip install -e .[dev]    # adds pytest, ruff, build, twine
pip install -e .[lxml]   # adds libxml2-bound parser (optional)
```

### Building a wheel locally

```sh
pip install build
python -m build
# → dist/fmddr-<version>-py3-none-any.whl
```

## Quickstart

```sh
# 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 `ExecuteSQL` or `Evaluate`?*
- *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 XML tree (grep/diff friendly)           |
| `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)

Edit a per-object split-tree XML file, then paste straight into FileMaker.

| Command                                       | Purpose                                                                 |
| --------------------------------------------- | ----------------------------------------------------------------------- |
| `fmddr copy <split-xml-path>`                 | Validate, wrap as `<fmxmlsnippet>`, push to OS clipboard                |
| `fmddr copy <script-xml> --steps all\|5-12`   | Copy script steps (all or a subset) as a ScriptStep — paste into a script |
| `fmddr paste [--type Script\|Table\|...]`     | Read FM clipboard XML and emit `<fmxmlsnippet>`                         |
| `fmddr validate <split-xml-path>`             | Run pre-clipboard guards only (no clipboard side effects)               |
| `fmddr clipboard formats`                     | List FM-recognized clipboard formats currently present                  |

Supports `<Script>`, `<BaseTable>`, `<CustomFunction>` roots. See [`docs/guides/clipboard`](./docs/src/content/docs/guides/clipboard.md).

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

```json
{
  "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:

```sh
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/`](./docs):

```sh
cd docs
npm install
npm run dev          # http://localhost:4321
npm run build        # static site → docs/dist/, deployable anywhere
```

## License

MIT
