saxml4adt 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.
- saxml4adt-0.1.0/LICENSE +21 -0
- saxml4adt-0.1.0/PKG-INFO +555 -0
- saxml4adt-0.1.0/README.md +529 -0
- saxml4adt-0.1.0/pyproject.toml +55 -0
- saxml4adt-0.1.0/saxml4adt/__init__.py +3 -0
- saxml4adt-0.1.0/saxml4adt/calc.py +169 -0
- saxml4adt-0.1.0/saxml4adt/cli.py +3069 -0
- saxml4adt-0.1.0/saxml4adt/conventions.py +374 -0
- saxml4adt-0.1.0/saxml4adt/db.py +495 -0
- saxml4adt-0.1.0/saxml4adt/export.py +281 -0
- saxml4adt-0.1.0/saxml4adt/ingest.py +1665 -0
- saxml4adt-0.1.0/saxml4adt/mcp.py +584 -0
- saxml4adt-0.1.0/saxml4adt/ops/install.ndjson +11 -0
- saxml4adt-0.1.0/saxml4adt/perf.py +469 -0
- saxml4adt-0.1.0/saxml4adt/queries.py +2558 -0
- saxml4adt-0.1.0/saxml4adt/record.py +144 -0
- saxml4adt-0.1.0/saxml4adt/render.py +1433 -0
- saxml4adt-0.1.0/saxml4adt/sample.py +336 -0
- saxml4adt-0.1.0/saxml4adt/snapshot.py +432 -0
- saxml4adt-0.1.0/saxml4adt/steps.py +393 -0
- saxml4adt-0.1.0/saxml4adt/styles.py +151 -0
- saxml4adt-0.1.0/saxml4adt/util.py +18 -0
- saxml4adt-0.1.0/saxml4adt/versions.py +176 -0
- saxml4adt-0.1.0/saxml4adt/watch.py +116 -0
- saxml4adt-0.1.0/saxml4adt/web/index.html +2479 -0
- saxml4adt-0.1.0/saxml4adt/web.py +1091 -0
- saxml4adt-0.1.0/saxml4adt.egg-info/PKG-INFO +555 -0
- saxml4adt-0.1.0/saxml4adt.egg-info/SOURCES.txt +45 -0
- saxml4adt-0.1.0/saxml4adt.egg-info/dependency_links.txt +1 -0
- saxml4adt-0.1.0/saxml4adt.egg-info/entry_points.txt +2 -0
- saxml4adt-0.1.0/saxml4adt.egg-info/requires.txt +7 -0
- saxml4adt-0.1.0/saxml4adt.egg-info/top_level.txt +1 -0
- saxml4adt-0.1.0/setup.cfg +4 -0
- saxml4adt-0.1.0/tests/test_console_ia.py +358 -0
- saxml4adt-0.1.0/tests/test_export_atlas.py +79 -0
- saxml4adt-0.1.0/tests/test_export_note.py +71 -0
- saxml4adt-0.1.0/tests/test_history_web.py +80 -0
- saxml4adt-0.1.0/tests/test_init.py +53 -0
- saxml4adt-0.1.0/tests/test_mcp.py +156 -0
- saxml4adt-0.1.0/tests/test_media.py +434 -0
- saxml4adt-0.1.0/tests/test_multifile.py +468 -0
- saxml4adt-0.1.0/tests/test_persistent_store.py +255 -0
- saxml4adt-0.1.0/tests/test_rollup_and_drift.py +262 -0
- saxml4adt-0.1.0/tests/test_snapshot.py +407 -0
- saxml4adt-0.1.0/tests/test_svg_sanitize.py +187 -0
- saxml4adt-0.1.0/tests/test_units.py +1298 -0
- saxml4adt-0.1.0/tests/test_wk8c_fixture.py +47 -0
saxml4adt-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Wallace / Empowered Data Solutions
|
|
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.
|
saxml4adt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: saxml4adt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Query a FileMaker Save-as-XML export: cross-references, readable scripts, impact analysis
|
|
5
|
+
Author-email: Michael Wallace <michael.wallace@empoweredds.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mw777eds/SaXML4ADT
|
|
8
|
+
Project-URL: Repository, https://github.com/mw777eds/SaXML4ADT
|
|
9
|
+
Keywords: filemaker,save-as-xml,cli,cross-reference
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: lxml>=5.0.0
|
|
20
|
+
Requires-Dist: typer>=0.9.0
|
|
21
|
+
Requires-Dist: keyring>=24.0
|
|
22
|
+
Requires-Dist: rich>=13.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# SaXML4ADT
|
|
28
|
+
|
|
29
|
+
**Save-as-XML for Agentic Development Toolkit.** A command-line query store over a
|
|
30
|
+
FileMaker **Save a Copy as XML** export: exact cross-references, readable scripts,
|
|
31
|
+
impact analysis, a freshness model, and a web console — built so an agent doing
|
|
32
|
+
FileMaker development with [`fm-cli`](https://github.com/claris/adt) can answer
|
|
33
|
+
"what will this touch?" in milliseconds, without taking FileMaker's schema lock,
|
|
34
|
+
and can tell when its answer is out of date.
|
|
35
|
+
|
|
36
|
+
Read-only. `fm-cli` stays the only writer. See [`docs/USE-CASES.md`](docs/USE-CASES.md)
|
|
37
|
+
for the use cases this was built against and the FileMaker quirks found on the way.
|
|
38
|
+
|
|
39
|
+
The idea of a queryable store over FileMaker's own structural export came from
|
|
40
|
+
[Nuosis/FM2WEB_CLI](https://github.com/Nuosis/FM2WEB_CLI) (a DDR toolkit); SaXML4ADT
|
|
41
|
+
is a from-scratch implementation around Save-as-XML and shares no code with it.
|
|
42
|
+
|
|
43
|
+
## Prerequisites
|
|
44
|
+
|
|
45
|
+
| | |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Python 3.10+ | `pipx` recommended |
|
|
48
|
+
| An ADT project | a folder with `adt.json` naming the file — the ADT toolkit's own `adt init`, not this tool's `saxml4adt init` (see *Install* below) — and [`fm-cli`](https://github.com/claris/adt) on `PATH` with its keychain credentials for the file (`fm --file=… --username=… --store-credentials`) |
|
|
49
|
+
| FileMaker Server with the **Data API** on | and an account with the `fmrest` extended privilege (the same account `adt.json` names is fine). Not hosted? See *Working without a server* below |
|
|
50
|
+
| Any coding agent | Claude Code gets the freshness hook (`saxml4adt init`); agents without hooks (Codex, …) run fm-cli through `saxml4adt fm …`, which marks for them — `saxml4adt init --agents` writes that protocol into `AGENTS.md` |
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pipx install git+https://github.com/mw777eds/SaXML4ADT # or: pip install git+…
|
|
56
|
+
cd ~/projects/my-adt-project # the folder with adt.json (or an empty folder)
|
|
57
|
+
saxml4adt serve # opens the browser on the Setup screen
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The Setup screen does the rest: server / file / account (saved to `adt.json`, password to the
|
|
61
|
+
credential store), readiness checks, and buttons for *Set up this project*, *Install into the
|
|
62
|
+
FileMaker file* and *Export and build* — the console appears when the first build lands. The same
|
|
63
|
+
steps from the terminal:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
saxml4adt init # hook + .gitignore + readiness check
|
|
67
|
+
saxml4adt install # once per FileMaker file (see below)
|
|
68
|
+
saxml4adt export --save # first time: asks for the password, stores it
|
|
69
|
+
saxml4adt build
|
|
70
|
+
saxml4adt serve
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
In an empty folder (no `adt.json` yet), `saxml4adt init --target fmnet://fms.example.com/MyFile --username admin`
|
|
74
|
+
writes one before the rest of `init` runs — the same thing `saxml4adt serve`'s Setup screen does from a form.
|
|
75
|
+
|
|
76
|
+
`init` writes the hook into `.claude/settings.json`, adds the export folder and
|
|
77
|
+
the store to `.gitignore` (they are client data), and reports what is missing.
|
|
78
|
+
`install` uses fm-cli to add one table (`SaXML4ADT_Export`), one layout of the
|
|
79
|
+
same name, and three scripts in a `SaXML4ADT` folder — the machinery that runs
|
|
80
|
+
Save a Copy as XML on the server and hands the files back through the Data API.
|
|
81
|
+
Everything it adds is in [`saxml4adt/ops/install.ndjson`](saxml4adt/ops/install.ndjson)
|
|
82
|
+
for review. Put the server's **full host name** in `adt.json`
|
|
83
|
+
(`fmnet://fms.example.com/File`) so the Data API certificate matches; a short
|
|
84
|
+
alias can be mapped in `~/.config/saxml4adt/hosts.json` (`{"fms": "fms.example.com"}`).
|
|
85
|
+
|
|
86
|
+
Credentials go in the OS credential store (macOS Keychain as a `saxml4adt: host/file`
|
|
87
|
+
item; `keyring` elsewhere), or in `SAXML4ADT_FM_PASSWORD` for CI.
|
|
88
|
+
`saxml4adt credentials` shows whether one is stored; `--forget` removes it and `--username NEW --save`
|
|
89
|
+
stores the next one. To change the account fm-cli edits as, update `username` in `adt.json`
|
|
90
|
+
(or run `adt connect`) and `fm --forget-credentials` / `--store-credentials` its own entry —
|
|
91
|
+
SaXML4ADT never reads or writes FileMaker's keychain items.
|
|
92
|
+
|
|
93
|
+
**Renamed from `saxml`.** Earlier builds installed a `saxml` command; it is gone. An agent whose
|
|
94
|
+
`saxml …` calls start failing with `command not found` mid-session is seeing the rename, not a
|
|
95
|
+
broken install — switch to `saxml4adt` (same subcommands) and re-run `saxml4adt init` so the
|
|
96
|
+
hook and .gitignore entries carry the new name.
|
|
97
|
+
|
|
98
|
+
## Help
|
|
99
|
+
|
|
100
|
+
`saxml4adt help` lists topics, `saxml4adt help scripts` the commands in one, `saxml4adt help steps` one
|
|
101
|
+
command with its options and examples (`--json` for agents). `saxml4adt <command> --help` still works.
|
|
102
|
+
`saxml4adt help concepts` lists the mental models behind the store (marked vs unmarked, freshness,
|
|
103
|
+
the hook loop, …); `saxml4adt help concepts marked-vs-unmarked` prints one.
|
|
104
|
+
|
|
105
|
+
## The loop
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
saxml4adt export → saxml4adt build → query / serve
|
|
109
|
+
▲ │
|
|
110
|
+
└── agent edits with fm → hook marks dirty ─┘
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
* **export** runs the server-side Save a Copy as XML and pulls every catalog file
|
|
114
|
+
into `XML/SaveAsXML/<File>/` (`--catalogs ScriptCatalog,LayoutCatalog` for a
|
|
115
|
+
partial export, merged into the folder). `--transport ssh` rsyncs the server's
|
|
116
|
+
Documents folder instead, for hosts you can reach. `pull` downloads an export
|
|
117
|
+
the server already produced (after `export --keep`) without running it again.
|
|
118
|
+
A `note` in the output when fewer files loaded than expected usually means
|
|
119
|
+
`Summary.xml` — every structural catalog still loaded; it's not FileMaker
|
|
120
|
+
skipping an empty one, and SaXML4ADT never reads `Summary.xml` anyway.
|
|
121
|
+
* **build** loads the export into `saxml4adt.sqlite` **in place**: the build
|
|
122
|
+
history, the change log and the dirty marks survive, and every object that
|
|
123
|
+
changed since the previous build is reconciled against the marks (`--fresh`
|
|
124
|
+
wipes). Seconds, even for a 70 MB export.
|
|
125
|
+
* **serve** opens the console: a map of catalogs, which objects are marked
|
|
126
|
+
dirty / changed / changed-without-a-mark, drill-down to any object, the op
|
|
127
|
+
that caused each mark, history by account, and the query log. It follows the
|
|
128
|
+
store live. The server runs in the background — the terminal is free.
|
|
129
|
+
Only one tab opens per project: running `serve` again while it's already up
|
|
130
|
+
prints the existing URL and does not start a second server or open another
|
|
131
|
+
tab; `serve --restart` stops and restarts it in place (same port) without
|
|
132
|
+
opening a tab either — the console page you already have open detects the
|
|
133
|
+
restart and reconnects on its own. `serve --stop` (or the title menu, which
|
|
134
|
+
also closes that tab) ends it, and it quits by itself after 12 hours without
|
|
135
|
+
a visitor.
|
|
136
|
+
* Every query prints JSON with `meta.stale` — the open marks the result touches —
|
|
137
|
+
and warns; `--strict` exits 3 on a stale answer, `--max-age 30m` exits 4 on an
|
|
138
|
+
old export, and `status` reports catalogs whose source files are gone or newer
|
|
139
|
+
than the build.
|
|
140
|
+
|
|
141
|
+
## What it answers that `fm` cannot
|
|
142
|
+
|
|
143
|
+
| | |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `saxml4adt steps <script>` | every step with **0-based index and 1-based line**, options **decoded by name** — including the 51 step types `fm` exposes only as numbered slots (the XML's `position="N"` equals fm's slot index), and calcs `fm` renders as `<Function Missing>` |
|
|
146
|
+
| `saxml4adt function <name>` | custom function bodies (`fm` cannot read them) |
|
|
147
|
+
| `saxml4adt broken-refs` | dangling Field/Script/Layout/TO references, including ones on layout objects that FileMaker's own *problems* list omits |
|
|
148
|
+
| `saxml4adt themes` / `styles <theme>` | every theme in the file and its named styles — the only names `create:layout` / `addObjects` will accept |
|
|
149
|
+
| `saxml4adt duplicates script` | name collisions `fm` cannot address individually |
|
|
150
|
+
| `saxml4adt taborder <layout>` | the tab order as FileMaker stores it — per-object positions, gaps left by deleted objects, and objects that have **no position at all** (everything `fm` adds), which is why fm-added fields land unpredictably; `fm` has no tab-order surface |
|
|
151
|
+
|
|
152
|
+
## What it answers faster
|
|
153
|
+
|
|
154
|
+
`usages Table::field` · `refs-to layout X` · `callers` / `callees` / `triggers` ·
|
|
155
|
+
`describe layout|script|to|field` · `objects <layout>` (with parent ids and
|
|
156
|
+
enclosing path) · `variables --problems` / `variable $$name` (assignments vs reads, case-insensitive like FileMaker, spelling drift) · `search` (FTS5 over
|
|
157
|
+
steps, calcs, comments) · `path TO-A TO-B` · `relations --suspicious` ·
|
|
158
|
+
`steps-of-type "Send Mail" --where "No dialog=Off"` · `fields-on <layout>` ·
|
|
159
|
+
`portals` · `unreferenced` (never "unused" — read its caveat).
|
|
160
|
+
|
|
161
|
+
## Freshness: an export is a snapshot
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
saxml4adt describe layout Settings # read
|
|
165
|
+
fm --file=… ops.ndjson # write with fm-cli
|
|
166
|
+
saxml4adt invalidate --from-ops ops.ndjson --as agent-2 # mark what the batch touched
|
|
167
|
+
saxml4adt describe layout Settings # -> meta.stale warns; --strict exits 3
|
|
168
|
+
saxml4adt refresh ~/exports/MyFile/SaveAsXML --catalogs LayoutCatalog # re-ingest only that catalog
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
* `invalidate <kind> <name>` marks one object; `--from-ops` marks every write
|
|
172
|
+
target in an fm-cli batch; `invalidate catalog ScriptCatalog` marks a whole catalog.
|
|
173
|
+
* `ops.ndjson` (also what `impact` preflights, below) is fm-cli's own batch format — one write per
|
|
174
|
+
line, e.g. `{"op":"create:field","table":"PRF__Preferences","name":"smtpAddress","type":"text"}`.
|
|
175
|
+
* `--max-age 2h` refuses to answer from an export older than that (exit 4).
|
|
176
|
+
* `refresh --catalogs A,B` replaces only those catalogs from a new (possibly
|
|
177
|
+
partial) export and clears their marks. FileMaker can produce a partial export:
|
|
178
|
+
the *Save a Copy as XML* script step takes options JSON
|
|
179
|
+
`{"catalogs_included":["ScriptCatalog"],"split_catalogs":true}`.
|
|
180
|
+
* `hooks/saxml4adt-invalidate-after-fm.sh` is a Claude Code `PostToolUse` hook that
|
|
181
|
+
runs `invalidate --from-ops` automatically after any `fm …` command, so the
|
|
182
|
+
discipline does not depend on the agent remembering.
|
|
183
|
+
* Hook discipline that is still on the agent: **write ops to a file** (ops on
|
|
184
|
+
stdin/heredoc are invisible to the hook, which then marks every writable
|
|
185
|
+
catalog dirty), and **export `SAXML4ADT_ACTOR`** so marks and `touched --by` can
|
|
186
|
+
tell agents apart. The hook skips `--dry-run` and batches fm rolled back.
|
|
187
|
+
* Every object row records `modified_by`, `modified_at` and its `modifications`
|
|
188
|
+
counter from the XML, so with an agent-specific FileMaker account a re-export
|
|
189
|
+
also shows what that account touched.
|
|
190
|
+
|
|
191
|
+
## History and drift
|
|
192
|
+
|
|
193
|
+
Every object row carries the XML's own `modifications` counter and
|
|
194
|
+
`modified_by`/`modified_at`. When a later export is ingested (`build` or
|
|
195
|
+
`refresh`), SaXML4ADT diffs those against the previous state and records what the
|
|
196
|
+
export *showed* changed — then reconciles it against what agents *said* changed:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
saxml4adt refresh XML/SaveAsXML --catalogs ScriptCatalog # prints drift: changes / unmarked / marked_unchanged
|
|
200
|
+
saxml4adt drift # last refresh: predicted, changed-but-unmarked, marked-but-unchanged
|
|
201
|
+
saxml4adt history --by agent-bot --since 2026-08-28 # observed changes over time, with who and whether it was marked
|
|
202
|
+
saxml4adt touched --by agent-bot # straight from the export's stamps, no history needed
|
|
203
|
+
saxml4adt builds
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**`unmarked_change` is the alarm this whole model exists for**: three agents and a
|
|
207
|
+
person share one file, and the failure that actually happens is a peer writing to
|
|
208
|
+
a file you own without your knowing. `unmarked` changes mean someone edited without telling the store (an agent that
|
|
209
|
+
skipped `invalidate`, or a person in FileMaker Pro). `marked_unchanged` means a
|
|
210
|
+
mark was a false alarm or the write never landed. Give each agent its own
|
|
211
|
+
FileMaker account and the export becomes its audit trail.
|
|
212
|
+
|
|
213
|
+
### Every version, without keeping the XML
|
|
214
|
+
|
|
215
|
+
Each build also records what *changed inside* every object — attribute before/after, script
|
|
216
|
+
steps as line hunks — in `change_log`. Nothing else is kept, so the store stays small, yet any
|
|
217
|
+
version since the store's second build can be reconstructed by replaying the deltas backwards:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
saxml4adt history --kind script --object "Nightly Sync" # every build that changed it, with the deltas
|
|
221
|
+
saxml4adt show script "Nightly Sync" --at 3 # the script as build 3's export showed it
|
|
222
|
+
saxml4adt diff script "Nightly Sync" --from 3 --to 5 # attribute before/after + unified diff of steps
|
|
223
|
+
saxml4adt diff field "Contacts::email" --from 2 # …to current
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Works for every kind (`layout`, `layout_object`, `field`, `table_occurrence`, `value_list`,
|
|
227
|
+
`custom_function`, …); the console's object panel shows the same timeline.
|
|
228
|
+
|
|
229
|
+
Web-viewer persistent stores (`ADT [<app>]` and friends, under `PersistentStoreCatalog`) get the
|
|
230
|
+
same treatment: each build records the payload's sha1, size, modification count, account and
|
|
231
|
+
timestamp — never the payload — so a redeploy shows up as an ordinary `change_log` row (old/new
|
|
232
|
+
sha1 + size) via `history`/`show`/`diff`, same as any other object.
|
|
233
|
+
`saxml4adt status` also compares each `ADT [<app>]` entry's recorded sha1 against
|
|
234
|
+
`webviewer-apps/<app>/dist/index.html` on disk and reports it under a `webviewer` block, so a stale
|
|
235
|
+
deploy is visible without opening FileMaker.
|
|
236
|
+
|
|
237
|
+
## Web console
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
saxml4adt serve # background server + browser; `serve --stop` ends it, 12 h idle timeout
|
|
241
|
+
saxml4adt serve --restart # stop + start in place, same port, no new tab (the open tab reconnects)
|
|
242
|
+
saxml4adt serve --foreground --port 8770 # in this terminal instead (Ctrl-C stops it)
|
|
243
|
+
saxml4adt export-web console.html # self-contained snapshot with the data embedded
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Home is a review feed**: "since your last visit" — the builds you have not seen, then
|
|
247
|
+
one row per changed object with a pill (✓ marked / ⚠ unmarked / a twin-export notice), the
|
|
248
|
+
kind and name, one line of what changed (step hunks, moved objects, the attributes), and who
|
|
249
|
+
touched it when. The name opens the object; the what-changed line opens it on the panel that
|
|
250
|
+
shows that change. *Mark all reviewed* advances your last-seen build (kept per store in this
|
|
251
|
+
browser), and *show all builds →* is History. The catalog map moved one click away, to the
|
|
252
|
+
**Catalogs** tab and the rail.
|
|
253
|
+
|
|
254
|
+
From there: drill-down to objects with their marks, observed changes, steps, layout objects
|
|
255
|
+
and references; a history view with the per-refresh drift bars and who-touched-what; the
|
|
256
|
+
query log. Marks can be made from the page when served live.
|
|
257
|
+
|
|
258
|
+
Every interaction is one of three verbs, and no click loses your place: **select**
|
|
259
|
+
answers in place (clicking an object in a layout preview outlines it and fills an
|
|
260
|
+
inspector directly below — bounds, enclosure breadcrumb, theme style and local
|
|
261
|
+
overrides, field binding, last change; ⌥-click adds more; in a script, click a step
|
|
262
|
+
and shift-click another for a range), **peek** opens over the view and Esc restores it
|
|
263
|
+
exactly, and **navigate** is the only thing that reroutes. Every list that names an object
|
|
264
|
+
— references, where-used, a layout's objects, a variable's scripts — peeks first: a
|
|
265
|
+
slide-over brief with the facts, its last change and its reference counts, and one
|
|
266
|
+
**Open →** when you actually want to go there.
|
|
267
|
+
|
|
268
|
+
An **investigation trail** runs under the header: every deliberate navigation leaves a chip
|
|
269
|
+
(kind + name), and clicking one returns to that stop with its scroll position intact — an
|
|
270
|
+
agent's stop lands there too, in amber. Back means the previous chip; Home starts a fresh
|
|
271
|
+
trail. It is session-local and sits above the browser's own Back, which keeps working.
|
|
272
|
+
|
|
273
|
+
The pane boundaries are **drag handles** (rail | list | detail); widths persist per browser
|
|
274
|
+
and a double-click resets one. **☰** collapses the catalog rail to a one-column icon strip.
|
|
275
|
+
`docs/CONSOLE-IA.md` is the full contract, including every hash parameter a deep link can
|
|
276
|
+
carry.
|
|
277
|
+
|
|
278
|
+
### Pins and agent navigation
|
|
279
|
+
|
|
280
|
+
**📌 Pin** on any selection adds an entry to your pins — layout objects, a range of
|
|
281
|
+
script steps, a calc fragment — instead of replacing the last one. The header button
|
|
282
|
+
shows a live count and peeks the list. Say "look at my pins" and the agent reads all of
|
|
283
|
+
them in one pull with the `pinned_items` MCP tool; reading grays them into a short
|
|
284
|
+
history rather than deleting them, so they stay re-askable until you clear them. Nothing
|
|
285
|
+
is ever pasted into your prompt.
|
|
286
|
+
|
|
287
|
+
The other direction: an agent can put something on your screen with `console_show`,
|
|
288
|
+
which drives the open tab to a view and shows a toast saying what it is showing and why.
|
|
289
|
+
Your previous view and scroll are saved first, and **Esc** — or the `↩ Esc returns you`
|
|
290
|
+
chip that outlives the toast — puts you back.
|
|
291
|
+
|
|
292
|
+
## MCP server
|
|
293
|
+
|
|
294
|
+
`saxml4adt mcp` speaks the [Model Context Protocol](https://modelcontextprotocol.io) over stdio
|
|
295
|
+
(JSON-RPC 2.0, newline-delimited), so Claude Desktop, Claude Code, Cursor and any other MCP client
|
|
296
|
+
can query the store directly — without shelling out to `saxml4adt`. It is hand-rolled (stdlib +
|
|
297
|
+
the rest of this package only; no `mcp` package dependency) and read-only except for one tool,
|
|
298
|
+
`mark_dirty`, which writes to the store's own freshness ledger (never to the FileMaker file —
|
|
299
|
+
`fm-cli` stays the only writer of that).
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
saxml4adt mcp --print-config # Claude Desktop snippet + the `claude mcp add` line, with an absolute --db path
|
|
303
|
+
saxml4adt mcp --db ~/projects/my-adt-project/saxml4adt.sqlite # run it (a client normally launches this itself)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`--print-config` fills in an absolute path so the config works regardless of the client's working
|
|
307
|
+
directory:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
claude mcp add saxml4adt -- saxml4adt mcp --db /Users/you/projects/my-adt-project/saxml4adt.sqlite
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
or, in `claude_desktop_config.json`:
|
|
314
|
+
|
|
315
|
+
```json
|
|
316
|
+
{"mcpServers": {"saxml4adt": {"command": "saxml4adt", "args": ["mcp", "--db", "/Users/you/projects/my-adt-project/saxml4adt.sqlite"]}}}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Tools** — one per read command that matters for an agent mid-task, each taking the same option
|
|
320
|
+
names as its CLI counterpart and returning the same `{meta, …}` envelope (so `meta.stale` still
|
|
321
|
+
warns when a result touches a mark made since the export): `overview`, `status`, `search`,
|
|
322
|
+
`object` (= `describe`), `referenced_by` (= `refs-to`, impact analysis), `script` (= `steps`),
|
|
323
|
+
`layout_objects` (= `objects`), `layout_html`, `styling`, `style_suggestions`, `variables`,
|
|
324
|
+
`history`, `show_at` (= `show`), `diff`, `blame`, `perf`, `conventions_check` (= `conventions`),
|
|
325
|
+
`unreferenced`, `query_log`, and `mark_dirty` (= `invalidate`, the one write tool). Each result
|
|
326
|
+
comes back as `content: [{type: "text", text: <json>}]` plus a matching `structuredContent`.
|
|
327
|
+
|
|
328
|
+
Two more talk to the console rather than the export:
|
|
329
|
+
|
|
330
|
+
- **`pinned_items`** — the items the user pinned in the console for you to look at, each with a
|
|
331
|
+
ready-to-read text block and the structured data behind it. Reading marks them read (they stay
|
|
332
|
+
in the console, grayed); `{"include_read": true}` brings them back.
|
|
333
|
+
- **`console_show(to, note)`** — put something on the user's screen: drives their open console tab
|
|
334
|
+
to a view, announces itself in a toast, and leaves Esc pointing back at where they were. Reports
|
|
335
|
+
whether a tab actually applied it, and returns the full URL for when none is open. Needs the
|
|
336
|
+
console to be running (`saxml4adt serve`) and to be launched from the project folder.
|
|
337
|
+
|
|
338
|
+
**Resources**: `saxml4adt://overview` (the same JSON as the `overview` tool) and
|
|
339
|
+
`saxml4adt://layout/<name>.html` — one per layout, the same rendered standalone-HTML preview
|
|
340
|
+
`layout-html` writes to disk, with dummy sample data.
|
|
341
|
+
|
|
342
|
+
The server connects to the store lazily, so `initialize` / `tools/list` still answer before a
|
|
343
|
+
build exists; a tool call against a missing store comes back as that one call's error, not a dead
|
|
344
|
+
connection. Nothing but JSON-RPC ever reaches stdout — diagnostics go to stderr.
|
|
345
|
+
|
|
346
|
+
## Layout snapshots (macOS)
|
|
347
|
+
|
|
348
|
+
`saxml4adt snapshot` captures real FileMaker Pro layouts as PNGs — ground truth to compare
|
|
349
|
+
against `layout-html`'s renders. Once per run it raises the file's document window to the front
|
|
350
|
+
of FileMaker's own window list (see the requirements below — this step matters), then switches
|
|
351
|
+
the live layout with AppleScript, captures the document window with Quartz + `screencapture`
|
|
352
|
+
(works on an occluded window, steals no focus), and records the layout FileMaker actually loaded,
|
|
353
|
+
since an `OnLayoutLoad` trigger can redirect.
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
saxml4adt snapshot "Contact Detail" # one layout -> ./Snapshots/Contact Detail.png
|
|
357
|
+
saxml4adt snapshot --all --out Snapshots/Empowered_Beginning --compare # every layout + a side-by-side compare.html
|
|
358
|
+
saxml4adt snapshot "Contact Detail" "Invoice Detail" --file Empowered_Contacts --delay 2
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Requirements:
|
|
362
|
+
|
|
363
|
+
- macOS, with FileMaker Pro running and the target file already open (not just hosted — open in
|
|
364
|
+
this copy of Pro).
|
|
365
|
+
- Screen Recording permission for whichever app runs `saxml4adt` (Terminal, iTerm, etc.) — System
|
|
366
|
+
Settings → Privacy & Security → Screen Recording.
|
|
367
|
+
- The `fmextscriptaccess` extended privilege on the account signed in to the file, for AppleScript
|
|
368
|
+
layout control. Granting it does not take effect on an already-open file — close and reopen it
|
|
369
|
+
first.
|
|
370
|
+
- **`go to layout` and `get name of current layout of database "X"` both act on that database's
|
|
371
|
+
FRONT window — not necessarily the document window a user would recognize.** A hidden card
|
|
372
|
+
window (this package's own "ADT MCP Server Connector" window is a real example) can be
|
|
373
|
+
frontmost instead; when that happens, every switch and readback silently happens *there* while
|
|
374
|
+
the visible document window never moves, so every captured PNG comes back byte-identical
|
|
375
|
+
regardless of which layout was requested (`current layout` even reads back as matching, since
|
|
376
|
+
it is honestly reporting the connector window's layout — the mismatch only shows up as pixels
|
|
377
|
+
that never change). `snapshot` handles this itself: before the sweep it raises the document
|
|
378
|
+
window it found via Quartz to the front of FileMaker's own window list (`go to window`, or a
|
|
379
|
+
System Events `AXRaise` fallback if that verb isn't in FileMaker's dictionary — the fallback
|
|
380
|
+
needs Accessibility automation permission granted to the host running this command), so every
|
|
381
|
+
later switch/readback in the run actually targets it. If you ever see a run come back with every
|
|
382
|
+
PNG hashing identically, this raise is what to check first.
|
|
383
|
+
- A modal FileMaker dialog (a `Show Custom Dialog` step, the "Summarize" field dialog, …) blocking
|
|
384
|
+
a layout switch is also handled automatically — it is detected, the app is activated and Escape
|
|
385
|
+
is sent, and the switch is retried once before that one layout is given up on and the sweep
|
|
386
|
+
moves on (see `warning`/`dialog_layouts` in the JSON summary). One bad layout — a dialog that
|
|
387
|
+
doesn't clear, a timed-out AppleEvent, anything else — is always isolated to its own row; it
|
|
388
|
+
never aborts the rest of the sweep.
|
|
389
|
+
|
|
390
|
+
`--compare` also renders each captured layout with `layout-html --sample dummy` into the output
|
|
391
|
+
directory and writes `<out>/compare.html`: the FileMaker PNG and the HTML render side by side per
|
|
392
|
+
layout, for eyeballing fidelity. It is a plain local file — nothing gets published anywhere.
|
|
393
|
+
|
|
394
|
+
The JSON summary reports, per layout, `{layout, actual, png, width, height, warning?}`, plus a
|
|
395
|
+
top-level `dialog_layouts` list of any layouts that hit a modal dialog. `actual` and `warning`
|
|
396
|
+
only differ from the requested name when an `OnLayoutLoad` trigger redirected the view; `--all`
|
|
397
|
+
skips layout names starting with `.` and keeps going past a failed switch, so one bad layout does
|
|
398
|
+
not abort the run.
|
|
399
|
+
|
|
400
|
+
## Working without a server
|
|
401
|
+
|
|
402
|
+
A file open in FileMaker Pro (not hosted): **File → Save a Copy as XML…** (split
|
|
403
|
+
catalogs, include details) into `XML/SaveAsXML/<File>/`, then `saxml4adt build`.
|
|
404
|
+
Everything except `export` works the same.
|
|
405
|
+
|
|
406
|
+
## Preflight an fm-cli batch
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
saxml4adt impact ops.ndjson
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
For each op: does the target exist in the export, is its name ambiguous, what
|
|
413
|
+
references would a delete break, and do the table / occurrence / theme names it
|
|
414
|
+
mentions resolve — all before `fm` opens the file and takes the schema lock.
|
|
415
|
+
|
|
416
|
+
## Verify a write
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
saxml4adt build before/ --db a.sqlite && saxml4adt build after/ --db b.sqlite
|
|
420
|
+
saxml4adt diff-stores a.sqlite b.sqlite --script "Do Thing" --assert-only-changed 118 # exit 6 if anything else moved
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Conventions
|
|
424
|
+
|
|
425
|
+
`saxml4adt conventions` checks every table, table occurrence, field, script, variable and custom
|
|
426
|
+
function name against a naming-conventions file — `saxml4adt.conventions.json`, next to
|
|
427
|
+
`saxml4adt.sqlite` in the project folder:
|
|
428
|
+
|
|
429
|
+
```json
|
|
430
|
+
{
|
|
431
|
+
"version": 1,
|
|
432
|
+
"tokens": { "code": "[A-Z]{3}", "Code": "[A-Z][a-z]{2}", "lcode": "[a-z]{3}", "Name": "[A-Z][A-Za-z0-9]*", "name": "[a-z][A-Za-z0-9]*", "NAME": "[A-Z][A-Z0-9_]*" },
|
|
433
|
+
"rules": {
|
|
434
|
+
"table": { "pattern": "{code}__{Name}", "example": "CNT__Contacts" },
|
|
435
|
+
"table_occurrence": { "base": "{code}__{Name}", "related": "{lcode}_{code}__{Name}", "example": "cnt_ADR__Addresses" },
|
|
436
|
+
"field": { "pattern": "{name}", "example": "firstName" },
|
|
437
|
+
"field_global": { "pattern": "{name}_g", "example": "sessionId_g" },
|
|
438
|
+
"field_key": { "pattern": "_{name}", "example": "_contactId" },
|
|
439
|
+
"script": { "pattern": "{Name}", "example": "Contact_Create" },
|
|
440
|
+
"variable": { "pattern": "${name}", "example": "$contactId" },
|
|
441
|
+
"variable_global": { "pattern": "$${NAME}", "example": "$$CURRENT_USER" },
|
|
442
|
+
"custom_function": { "pattern": "{name}", "example": "trimAll" }
|
|
443
|
+
},
|
|
444
|
+
"ignore": ["^zz_", "^Global$"]
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Each `{token}` in a pattern substitutes the named regex from `tokens`; every other character is
|
|
449
|
+
literal (a `$` in `variable`/`variable_global` matches a literal `$`). A rule may give several
|
|
450
|
+
named alternatives instead of one `pattern` — `table_occurrence` above passes a name that matches
|
|
451
|
+
either `base` (a table occurrence standing in for its own base table) or `related` (one added for
|
|
452
|
+
a specific relationship context, prefixed with the other table's `lcode`) — and a name passes the
|
|
453
|
+
rule if *any* alternative fully matches. `ignore` is a list of regexes checked against a name
|
|
454
|
+
before it is checked at all (so `zz_Archive__Old` or a stock `Global` table are never flagged).
|
|
455
|
+
|
|
456
|
+
`field_global` covers fields with FileMaker's *Global storage* on; `field_key` covers fields whose
|
|
457
|
+
name looks like a key (`/^_/` or `/id$/i`, case-insensitive) **and** that are actually used on
|
|
458
|
+
either side of a relationship — a `customerNotes` field ending in nothing key-shaped stays a plain
|
|
459
|
+
`field` even if it happens to hold an ID-like value.
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
saxml4adt conventions --infer # guess one from the majority pattern already in the store
|
|
463
|
+
saxml4adt conventions --infer --write # save the guess as saxml4adt.conventions.json (--force to overwrite)
|
|
464
|
+
saxml4adt conventions # check every name — the default once the file exists
|
|
465
|
+
saxml4adt conventions --kind field -f table # one rule kind only
|
|
466
|
+
saxml4adt conventions --strict # exit 1 if anything violates
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
`--infer` classifies every existing name into a shape (`AAA__Xxx`, `aaa_AAA__Xxx`, `xxx`, `Xxx`,
|
|
470
|
+
`xxx_g`, `_xxx`, `XXX`, `Xxx_Xxx`, `xxx_xxx`, or `other`), picks the majority shape per rule kind,
|
|
471
|
+
and reports a `confidence` (the share of names that actually match it) plus the top-5 shape
|
|
472
|
+
histogram — review those before `--write`ing an inferred file over a real one. `--check` (the
|
|
473
|
+
default once a file exists and `--infer` is not given) reports every name that does not match, with
|
|
474
|
+
`expected` (the alternatives it was checked against) and, where trivially derivable — camelCasing a
|
|
475
|
+
field, uppercasing a global variable — a `suggestion`; `table` and `table_occurrence` violations get
|
|
476
|
+
no suggestion, since picking the right 3-letter code is a human call.
|
|
477
|
+
|
|
478
|
+
## Multi-file projects
|
|
479
|
+
|
|
480
|
+
A solution split across several `.fmp12` files — a UI file plus one or more data files wired
|
|
481
|
+
together with external data sources — builds into **one store holding all of them**, with
|
|
482
|
+
references resolved across the file boundary: a field in a data file used only by layouts in the
|
|
483
|
+
UI file no longer reads as unreferenced. `saxml4adt build --file A --file B` (repeatable) or a
|
|
484
|
+
`saxml4adt.project.json` listing `"files"` in the project root drives which files are ingested;
|
|
485
|
+
`saxml4adt files` lists what the store holds — objects per file, last build, and any unresolved
|
|
486
|
+
external data source. Every query that names an object takes `--file <name>` to narrow it to one
|
|
487
|
+
file; left off, the answer covers the whole project — or, once the store holds more than one file
|
|
488
|
+
and one is marked **primary**, the primary file, with `--all-files` (alias `--all`) restoring
|
|
489
|
+
store-wide scope for that one call. Single-file projects, and multi-file ones with no primary set,
|
|
490
|
+
are unaffected — `file_id 1` everywhere is exactly the old behavior, byte-identical.
|
|
491
|
+
|
|
492
|
+
Mark the development file with `saxml4adt files --set-primary Empowered_Beginning` (or a
|
|
493
|
+
`"primary"` key in `saxml4adt.project.json`, which wins when both are set; `--clear-primary`
|
|
494
|
+
removes the store's own mark). Once a primary is set, fm-cli write commands
|
|
495
|
+
(`variable --rename --apply`, `variables --fix-spelling --apply`, `uninstall`) refuse to target
|
|
496
|
+
any other file unless you pass `--file <that file>` explicitly — the primary is the file you are
|
|
497
|
+
developing; the rest are treated read-only from here. `export`/`install`/`uninstall --dry-run`
|
|
498
|
+
still work per-file regardless, since pulling or installing the export machinery on an old file is
|
|
499
|
+
harmless.
|
|
500
|
+
|
|
501
|
+
`build` also guards against **copy-twins**: a file whose export shares more than 20% of its object
|
|
502
|
+
uuids with a different file already in the store — the same file present twice under different
|
|
503
|
+
names, e.g. an old export left next to a newer one — is held for confirmation (or, without a
|
|
504
|
+
terminal, refused with exit 3) unless `--allow-twin` (alias `--yes`) is passed; `saxml4adt files`
|
|
505
|
+
reports any such pair it finds, with the overlap percentage.
|
|
506
|
+
|
|
507
|
+
Full design in [`docs/MULTI-FILE.md`](docs/MULTI-FILE.md).
|
|
508
|
+
|
|
509
|
+
## Boundaries
|
|
510
|
+
|
|
511
|
+
* Read-only. `fm-cli` remains the only writer.
|
|
512
|
+
* Structure only. A table can be wired everywhere and hold zero records; this
|
|
513
|
+
tool cannot see data, whether layout objects render, or business facts.
|
|
514
|
+
* `unreferenced` ≠ unused. Manual-by-design scripts, external callers (Data API,
|
|
515
|
+
WebDirect), and by-name runtime addressing are invisible here; targets that
|
|
516
|
+
*could* be addressed dynamically are reported `UNDECIDABLE`.
|
|
517
|
+
|
|
518
|
+
## Known FileMaker limits (found while building this)
|
|
519
|
+
|
|
520
|
+
* **Save-as-XML can omit objects fm-cli appended to an existing layout** —
|
|
521
|
+
the live layout has them, the export does not. `saxml4adt verify-layouts`
|
|
522
|
+
cross-checks every layout's object count against the live file through fm-cli.
|
|
523
|
+
* **fm-cli writes a blank modifier name** on everything it creates or updates,
|
|
524
|
+
and the first export overwrites that blank on *layouts* with the export session
|
|
525
|
+
(`SaXML Export - <account> <pid>`). Details: [`docs/bug-reports/`](docs/bug-reports/).
|
|
526
|
+
|
|
527
|
+
## Platforms
|
|
528
|
+
|
|
529
|
+
macOS and Windows (FileMaker Pro's platforms). macOS is what this is developed on; Windows uses the
|
|
530
|
+
same code paths — credentials via Windows Credential Manager (`keyring`), the hook as
|
|
531
|
+
`saxml4adt hook --as "…"`, and the container transport, which needs no ssh. Windows has had no
|
|
532
|
+
hands-on run yet: if something breaks, `saxml4adt init` prints the readiness check to include in an
|
|
533
|
+
issue. The `--transport ssh` path assumes a Linux FileMaker Server Documents path (`--remote-docs`
|
|
534
|
+
to override).
|
|
535
|
+
|
|
536
|
+
## Releasing
|
|
537
|
+
|
|
538
|
+
Tag a version and push it: `git tag v0.1.0 && git push origin v0.1.0`. The `publish` workflow builds
|
|
539
|
+
the wheel and uploads it to PyPI through trusted publishing (project `saxml4adt`, environment
|
|
540
|
+
`pypi`) — no tokens in the repo. After the first release, `pipx install saxml4adt` is the install line.
|
|
541
|
+
|
|
542
|
+
## Development
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
git clone https://github.com/mw777eds/SaXML4ADT && cd SaXML4ADT
|
|
546
|
+
python -m pip install -e '.[dev]'
|
|
547
|
+
python -m pytest -q # 11 unit tests on the committed fixture
|
|
548
|
+
SAXML4ADT_TEST_EXPORT=/path/to/an/export python -m pytest -q # + 6 tests against a real export
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Layout: `saxml4adt/export.py` (find and repair the XML), `calc.py` (tokenize
|
|
552
|
+
calcs), `steps.py` (decode script steps), `ingest.py` (build the store),
|
|
553
|
+
`queries.py` (every question), `conventions.py` (naming-conventions check + infer),
|
|
554
|
+
`cli.py` (typer app: commands, export transports, hook, init, install),
|
|
555
|
+
`web.py` + `web/index.html` (console), `mcp.py` (MCP stdio server).
|