simframe 0.4.2 → 0.5.0
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.
- package/README.md +227 -53
- package/native/simframed/Package.swift +16 -0
- package/native/simframed/Sources/PrivateAPI/CoreSimulatorPlatform.swift +449 -0
- package/native/simframed/Sources/PrivateAPI/HIDKeyboard.swift +70 -0
- package/native/simframed/Sources/PrivateAPI/IndigoHID.swift +121 -0
- package/native/simframed/Sources/PrivateAPI/PrivateAPI.swift +117 -0
- package/native/simframed/Sources/PrivateAPI/StubPlatform.swift +89 -0
- package/native/simframed/Sources/SimframeCore/Bitmap.swift +61 -0
- package/native/simframed/Sources/SimframeCore/ControlSocket.swift +122 -0
- package/native/simframed/Sources/SimframeCore/CoreGraphicsScaler.swift +70 -0
- package/native/simframed/Sources/SimframeCore/Element.swift +120 -0
- package/native/simframed/Sources/SimframeCore/FrameStore.swift +303 -0
- package/native/simframed/Sources/SimframeCore/Hashing.swift +119 -0
- package/native/simframed/Sources/SimframeCore/Motion.swift +431 -0
- package/native/simframed/Sources/SimframeCore/PNGWriter.swift +40 -0
- package/native/simframed/Sources/SimframeCore/VisionOCR.swift +75 -0
- package/native/simframed/Sources/simframed/main.swift +357 -0
- package/native/simframed/Tests/SimframeCoreTests/HashingTests.swift +240 -0
- package/package.json +10 -4
- package/scripts/bench-flow.mjs +54 -0
- package/scripts/bench.sh +98 -0
- package/scripts/check-package.mjs +91 -0
- package/scripts/smoke.mjs +76 -0
- package/scripts/verify-baseline.mjs +65 -0
- package/src/actions.js +82 -5
- package/src/cli.js +347 -31
- package/src/control.js +76 -0
- package/src/daemon.js +8 -1
- package/src/engine.js +99 -0
- package/src/fingerprint.js +150 -0
- package/src/graph.js +409 -0
- package/src/index.js +292 -23
- package/src/input.js +80 -2
- package/src/matching.js +194 -0
- package/src/mcp.js +45 -1
- package/src/navigate.js +120 -0
- package/src/regions.js +90 -0
- package/src/screenmap.js +77 -21
- package/src/simctl.js +20 -4
- package/src/store.js +8 -0
package/README.md
CHANGED
|
@@ -32,13 +32,25 @@ Same four-tab navigation flow, on a real production app:
|
|
|
32
32
|
| --- | --- | --- |
|
|
33
33
|
| Look at the screen | ~130–400 ms, blocking | **~20 ms**, already captured |
|
|
34
34
|
| "Did anything change?" | a full image | **~2 ms**, text only |
|
|
35
|
-
| A 5-step flow | 5+ model round trips | **1 call**, ~7 s |
|
|
36
35
|
| Finding a control | read tree (~570 ms) + reason | **~1 ms** from memory |
|
|
37
|
-
|
|
|
36
|
+
| A 4-step flow, verified | 4+ model round trips | **1 call**, 3.6 s |
|
|
37
|
+
| Same flow, 3rd run | no improvement — every run is the first | **3.7 s, 4/4 from memory, 4/4 verified** |
|
|
38
38
|
|
|
39
|
-
The
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
The four-tab tour, three times back to back from a cleared memory:
|
|
40
|
+
|
|
41
|
+
| Pass | Wall clock | Steps verified | Controls from memory |
|
|
42
|
+
| --- | --- | --- | --- |
|
|
43
|
+
| 1 | 10.2 s | 0/4 — nothing is known yet | **4/4** |
|
|
44
|
+
| 2 | **3.6 s** | **4/4** | **4/4** |
|
|
45
|
+
| 3 | **3.7 s** | **4/4** | **4/4** |
|
|
46
|
+
|
|
47
|
+
Every step is checked against what the same action did last time, and the run
|
|
48
|
+
records its own preconditions — which input path, which daemon, whether the
|
|
49
|
+
daemon was replaced mid-run — so a regression shows up in the measurement rather
|
|
50
|
+
than hiding inside it. Earlier versions of this table quoted 7.4 s → 3.3 s with
|
|
51
|
+
verification switched off; those numbers were measured while input was silently
|
|
52
|
+
falling back to a slower path and the capture daemon was being replaced by every
|
|
53
|
+
command, so they measured two bugs rather than the tool.
|
|
42
54
|
|
|
43
55
|
## Install
|
|
44
56
|
|
|
@@ -47,6 +59,11 @@ npm install -g simframe
|
|
|
47
59
|
simframe doctor
|
|
48
60
|
```
|
|
49
61
|
|
|
62
|
+
The first `simframe start` builds a small Swift daemon from source — a few
|
|
63
|
+
seconds, once. It needs the Xcode command line tools, which you already have if
|
|
64
|
+
you have a simulator. Without them simframe falls back to the original
|
|
65
|
+
`simctl` loop and says so.
|
|
66
|
+
|
|
50
67
|
`doctor` checks each capability separately and tells you what you have:
|
|
51
68
|
|
|
52
69
|
```
|
|
@@ -86,7 +103,12 @@ have. **Observation needs nothing but Xcode.**
|
|
|
86
103
|
| --- | --- | --- |
|
|
87
104
|
| Watch the screen, wait, recall | nothing extra | — |
|
|
88
105
|
| Read labels + coordinates from pixels | `swiftc` (Xcode CLT) | falls back to the accessibility tree alone |
|
|
89
|
-
| Tap, type, swipe | [`idb`](https://fbidb.io) | simframe observes but cannot touch |
|
|
106
|
+
| Tap, type, swipe | nothing extra, or [`idb`](https://fbidb.io) as a fallback | simframe observes but cannot touch |
|
|
107
|
+
| Accessibility tree | [`idb`](https://fbidb.io) | OCR alone still yields labels and coordinates |
|
|
108
|
+
|
|
109
|
+
`simframe doctor` names which engine is carrying each capability, per device.
|
|
110
|
+
**idb is now required only for the accessibility tree** — capture, input and text
|
|
111
|
+
recognition all run in-process.
|
|
90
112
|
|
|
91
113
|
```bash
|
|
92
114
|
# input, optional
|
|
@@ -144,7 +166,8 @@ So simframe reads the screen two ways and remembers the result:
|
|
|
144
166
|
- **Accessibility** gives real hit targets, types and enabled state.
|
|
145
167
|
- **On-device OCR** (Apple's Vision, ~290 ms, no model round trip) gives every
|
|
146
168
|
label a person can actually see, with coordinates.
|
|
147
|
-
- The merge is keyed by a **
|
|
169
|
+
- The merge is keyed by a **structural fingerprint**, so the next visit is a
|
|
170
|
+
file read. What that fingerprint is, and why it is not a pixel hash, is below.
|
|
148
171
|
|
|
149
172
|
```
|
|
150
173
|
first visit to a screen ~1000 ms read tree + OCR, store the map
|
|
@@ -164,34 +187,92 @@ Two details that matter:
|
|
|
164
187
|
a tab returns an error listing both with coordinates, because silently tapping
|
|
165
188
|
the title looks exactly like nothing happening.
|
|
166
189
|
|
|
167
|
-
###
|
|
190
|
+
### Two hashes, because there are two questions
|
|
168
191
|
|
|
192
|
+
"Did this move?" and "is this the same screen?" look like one question and are
|
|
193
|
+
not. simframe answers them separately, and getting that wrong was the single
|
|
194
|
+
most expensive mistake in its development.
|
|
195
|
+
|
|
196
|
+
**Change and settle** are questions about pixels, so a pixel hash answers them.
|
|
169
197
|
The frame hash changes whenever any pixel group changes — a clock digit, one new
|
|
170
|
-
row
|
|
171
|
-
|
|
172
|
-
12×24 grid.
|
|
198
|
+
row — which is exactly right for "did anything happen?" and useless as a key for
|
|
199
|
+
"have I been here before?". For change detection there is a layout hash: status
|
|
200
|
+
bar cropped, difference hash over a 12×24 grid.
|
|
173
201
|
|
|
174
202
|
A mean-threshold hash was tried first and was actively dangerous: low-contrast
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
203
|
+
screens collapsed onto identical values, so unrelated screens matched at distance
|
|
204
|
+
0 and taps landed on the wrong control. The difference hash fixed that.
|
|
205
|
+
|
|
206
|
+
**Identity is not a question about pixels**, and this is the part that took three
|
|
207
|
+
attempts. Content *is* pixels: a list whose rows changed drifts as far as a
|
|
208
|
+
different screen does. Measured on a real app, same-screen revisits reached 62
|
|
209
|
+
bits against a different-screen floor of 74 — overlapping, with no threshold
|
|
210
|
+
available to choose. An earlier calibration had suggested a comfortable margin
|
|
211
|
+
(0–4 against 77–113), but it was measured on screens whose content happened to be
|
|
212
|
+
stable and did not survive contact with a real list.
|
|
213
|
+
|
|
214
|
+
So identity is **structural**. The fingerprint is built from element roles,
|
|
215
|
+
frames quantised to a 24 px grid, the region each element sits in, and repeated
|
|
216
|
+
siblings bucketed as "one" or "many" rather than counted. Deliberately included:
|
|
217
|
+
the labels of chrome elements only — nav title, tab labels, toolbar buttons —
|
|
218
|
+
because two list screens with identical structure are told apart by their title
|
|
219
|
+
and nothing else. Deliberately excluded: all content text and values, the status
|
|
220
|
+
bar, and the keyboard region when a keyboard is up.
|
|
221
|
+
|
|
222
|
+
It does not depend on the accessibility tree. Fingerprinting from OCR boxes
|
|
223
|
+
alone, with the tree discarded entirely, still separates screens — different
|
|
224
|
+
screens ceiling 0.35 against the same threshold.
|
|
225
|
+
|
|
226
|
+
| | Jaccard similarity |
|
|
179
227
|
| --- | --- |
|
|
180
|
-
| Same screen, revisited
|
|
181
|
-
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
228
|
+
| Same screen, revisited | 0.41–1.00 |
|
|
229
|
+
| **Different screens** | **0.00–0.31** |
|
|
230
|
+
|
|
231
|
+
The threshold sits in that gap, but the gap is narrower than anyone would want,
|
|
232
|
+
and one screen causes it: a screen whose sections load from different sources has
|
|
233
|
+
more than one genuine settled structure, and two structures of one screen are as
|
|
234
|
+
far apart as two different screens.
|
|
235
|
+
|
|
236
|
+
No threshold can express that, so a screen may hold **several** accepted
|
|
237
|
+
fingerprints instead. A new one is admitted only when a known edge lands
|
|
238
|
+
somewhere its target does not recognise — the edge is the evidence that it is the
|
|
239
|
+
same place — and only if no other stored screen claims that reading. Identity
|
|
240
|
+
stays exact rather than being loosened, and the count is capped, so a
|
|
241
|
+
non-deterministic action shows up as a node collecting variants rather than as
|
|
242
|
+
screens silently merging.
|
|
243
|
+
|
|
244
|
+
It earns its place on real apps: an app reconnecting to its bundler put an alert
|
|
245
|
+
over one screen, and that screen gained a variant instead of a duplicate
|
|
246
|
+
appearing.
|
|
247
|
+
|
|
248
|
+
Failing to recognise a screen you have seen is harmless — it rebuilds the map and
|
|
249
|
+
taps correctly. Matching the *wrong* screen taps the wrong control. The threshold
|
|
250
|
+
is set to err toward the first.
|
|
251
|
+
|
|
252
|
+
## Navigating by memory
|
|
253
|
+
|
|
254
|
+
Once simframe knows which screens exist and which action leads from one to the
|
|
255
|
+
next, getting somewhere is a search over known edges rather than a question for a
|
|
256
|
+
model:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
simframe screens # what this device has learned
|
|
260
|
+
simframe goto invoices # walk there, verifying every step
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Measured on a four-tab tour, `goto` plans and walks three-step routes with every
|
|
264
|
+
step verified and no model call. It fails rather than guesses: an unknown
|
|
265
|
+
destination, a query matching two screens equally, or no path of known edges all
|
|
266
|
+
report themselves instead of tapping hopefully.
|
|
267
|
+
|
|
268
|
+
Flows work the same way and refuse to save if any step went unverified —
|
|
269
|
+
replaying a recording of something that may not have worked just reproduces the
|
|
270
|
+
doubt.
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
simframe flow save checkout ./checkout.json
|
|
274
|
+
simframe flow run checkout
|
|
275
|
+
```
|
|
195
276
|
|
|
196
277
|
## Does this work on *your* app?
|
|
197
278
|
|
|
@@ -219,37 +300,65 @@ iPhone 17 Pro, iOS 26.5, Apple Silicon, default settings.
|
|
|
219
300
|
|
|
220
301
|
| | |
|
|
221
302
|
| --- | --- |
|
|
303
|
+
| Frame capture, whole pipeline | **6.6 ms** |
|
|
304
|
+
| Frame grab alone | **0.13 ms** |
|
|
305
|
+
| The `simctl` + `sips` path it replaces | ~210 ms |
|
|
222
306
|
| Warm frame read (`sim_look`) | ~20 ms |
|
|
223
307
|
| State check (`sim_state`) | ~2 ms |
|
|
224
|
-
|
|
|
225
|
-
|
|
|
226
|
-
|
|
|
227
|
-
|
|
|
228
|
-
|
|
|
229
|
-
|
|
|
308
|
+
| Input round trip (`ping`) | **0 ms** |
|
|
309
|
+
| Tap (70 ms hold / 10 ms hold) | 76 ms / 13 ms |
|
|
310
|
+
| Text recognition, in-process | **~174 ms** |
|
|
311
|
+
| Text recognition, via PNG + helper (fallback) | ~555 ms |
|
|
312
|
+
| Accessibility tree read (idb) | ~570 ms |
|
|
313
|
+
| Screen map: first visit / remembered | ~305 ms / **~1 ms** |
|
|
230
314
|
| CPU | 1.1 % idle · 3.1 % active |
|
|
231
315
|
| Frame memory | ~60 s of screen, ~2.7 MB |
|
|
232
316
|
|
|
317
|
+
Reproduce all of it with `npm run bench`, which prints the same table against
|
|
318
|
+
your machine. Full detail, including the measurement traps, is in
|
|
319
|
+
[`docs/BENCHMARKS.md`](docs/BENCHMARKS.md).
|
|
320
|
+
|
|
233
321
|
## How it works
|
|
234
322
|
|
|
235
323
|
```
|
|
236
|
-
|
|
237
|
-
│
|
|
238
|
-
│
|
|
324
|
+
┌─── simframed — one Swift daemon per simulator ──────────────────────────────┐
|
|
325
|
+
│ │
|
|
326
|
+
│ display damage callback ──► read IOSurface ──► scale ──► hash │
|
|
327
|
+
│ the screen tells us 0.13 ms 6.6 ms total │
|
|
328
|
+
│ │ │
|
|
329
|
+
│ Vision OCR reads the same surface ─────┤ no PNG, no file, no spawn │
|
|
330
|
+
│ │ │
|
|
331
|
+
│ Indigo HID ◄── control socket ◄────────┤ 0600, one JSON object per line │
|
|
332
|
+
│ taps, swipes, text │ │
|
|
333
|
+
│ ▼ │
|
|
334
|
+
│ ~/.simframe/<udid>/ frames · state.json · meta.json │
|
|
239
335
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
240
336
|
│ a rename is atomic
|
|
241
337
|
┌────────────────────────────────────▼────────────────────────────────────────┐
|
|
242
|
-
│ MCP server / CLI: stat + read
|
|
243
|
-
│ Screen memory: layout hash ──► label → point, built once per screen.
|
|
338
|
+
│ MCP server / CLI: stat + read, or one socket round trip for input. │
|
|
339
|
+
│ Screen memory: layout hash ──► label → point, built once per screen. │
|
|
244
340
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
245
341
|
```
|
|
246
342
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
343
|
+
The daemon links CoreSimulator and SimulatorKit, which are private frameworks
|
|
344
|
+
with no documentation and no stability promise. Everything it calls is recorded
|
|
345
|
+
in [`docs/PRIVATE_API.md`](docs/PRIVATE_API.md) with the evidence behind it, so
|
|
346
|
+
an Xcode upgrade that moves something is a bounded fix rather than an
|
|
347
|
+
archaeology project. If a layer breaks, simframe degrades to the layer below
|
|
348
|
+
and `doctor` says which.
|
|
349
|
+
|
|
350
|
+
Run `simframe start --engine=simctl` to use the original loop instead.
|
|
351
|
+
|
|
352
|
+
- **Files are the IPC for reads.** The daemon renames completed frames into
|
|
353
|
+
place; readers just read them. A rename is atomic, so a reader can never see a
|
|
354
|
+
half-written frame. Input is the one thing that needs a reply, and it goes
|
|
355
|
+
over a `0600` Unix socket — the file system is the whole permission model.
|
|
356
|
+
- **Almost no dependencies.** The only runtime npm dependency is the MCP SDK.
|
|
357
|
+
The daemon is Swift built from source against frameworks already on the
|
|
358
|
+
machine.
|
|
359
|
+
- **The screen says when it changed.** The capture loop is driven by the
|
|
360
|
+
display's damage callback rather than a timer, so an idle screen costs
|
|
361
|
+
nothing and a moving one is picked up at once.
|
|
253
362
|
- **It backs off when nothing happens.** 4 fps while the screen moves, 1.5 fps
|
|
254
363
|
once still, snapping back instantly on change.
|
|
255
364
|
- **One writer per device.** Ownership lives in `meta.json`; `stop` refuses to
|
|
@@ -273,14 +382,72 @@ simframe ui # labels + tap points (ax and ocr)
|
|
|
273
382
|
simframe recall # what happened in the last minute
|
|
274
383
|
simframe recall --ago=15000 # the frame from 15s ago
|
|
275
384
|
simframe strip --count=6 # contact sheet
|
|
385
|
+
simframe screens # screens this device has learned
|
|
386
|
+
simframe goto invoices # walk to a known screen over known steps
|
|
387
|
+
simframe flow save|run|list # record a verified flow, replay it
|
|
388
|
+
simframe doctor --json machine-readable; --strict fails on any downgrade
|
|
276
389
|
simframe status / stop [--force] / devices / doctor
|
|
277
390
|
```
|
|
278
391
|
|
|
392
|
+
## Degrading is allowed. Degrading quietly is not
|
|
393
|
+
|
|
394
|
+
simframe is built to degrade rather than fail: no Swift toolchain still gives
|
|
395
|
+
you frames through `simctl`, no accessibility tree still gives you OCR. That
|
|
396
|
+
policy is right, and it nearly sank the tool twice — because a downgrade looked
|
|
397
|
+
exactly like everything working.
|
|
398
|
+
|
|
399
|
+
Once, one file was missing from the published package, so `Package.swift`
|
|
400
|
+
declared a test target with no directory, SwiftPM reported overlapping sources,
|
|
401
|
+
and **every install silently fell back to the slow engine**. Another time OCR
|
|
402
|
+
shipped disabled the same way. Both passed the tests. Both printed nothing. The
|
|
403
|
+
bug was never the missing file; it was the silence.
|
|
404
|
+
|
|
405
|
+
So every downgrade now announces itself:
|
|
406
|
+
|
|
407
|
+
- `simframe start` prints the engine it chose, and if it is the slow one, why —
|
|
408
|
+
build error, missing sources, or "reason unrecorded" if the daemon was started
|
|
409
|
+
by an earlier process.
|
|
410
|
+
- `simframe doctor` marks a degraded layer `WARN`, not `ok`, and summarises what
|
|
411
|
+
is degraded and what that costs.
|
|
412
|
+
- A dependency that is simply not installed is `--`, not `WARN`. The distinction
|
|
413
|
+
is deliberate: `WARN` means this machine could be doing better and silently is
|
|
414
|
+
not, which is the failure worth shouting about. idb missing on a fresh machine
|
|
415
|
+
has not degraded from anything, and `--strict` ignores it.
|
|
416
|
+
- `--strict`, or `SIMFRAME_STRICT=1`, turns any downgrade into a non-zero exit.
|
|
417
|
+
CI runs strict, so a release cannot ship in the state that shipped twice.
|
|
418
|
+
|
|
419
|
+
```
|
|
420
|
+
$ simframe doctor
|
|
421
|
+
ok capture engine simframed
|
|
422
|
+
WARN input driver idb — the daemon's control socket is not up
|
|
423
|
+
-- accessibility tree not installed: idb is not installed
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Two checks enforce it. A packaging check derives the required file list from the
|
|
427
|
+
build's own inputs — a hand-written list is what rotted last time — and runs in
|
|
428
|
+
seconds without a simulator. An integration job installs the packed tarball on a
|
|
429
|
+
real simulator and asserts `capture.engine`, `input.driver` and `ocr.available`
|
|
430
|
+
are all the good values, under `--strict`.
|
|
431
|
+
|
|
432
|
+
That last check found a real bug the day it was written: a daemon shutting down
|
|
433
|
+
unlinked the control socket unconditionally, so restarting deleted the *new*
|
|
434
|
+
daemon's socket. Capture kept working, input quietly dropped to idb, and nothing
|
|
435
|
+
said a word — the exact failure shape, found by the thing built to catch it.
|
|
436
|
+
|
|
279
437
|
## Limitations
|
|
280
438
|
|
|
281
|
-
- Simulators only
|
|
282
|
-
|
|
283
|
-
|
|
439
|
+
- Simulators only. Neither the framebuffer nor `simctl` can reach a physical
|
|
440
|
+
device.
|
|
441
|
+
- The daemon depends on private frameworks. They are stable enough to build on —
|
|
442
|
+
capture and accessibility survived the iOS 26 transition — but an Xcode
|
|
443
|
+
upgrade can move a symbol. `doctor` reports each layer separately so a break
|
|
444
|
+
is visible rather than mysterious, and `--engine=simctl` still works.
|
|
445
|
+
- Hardware buttons: only `home` is implemented. The other Indigo codes are
|
|
446
|
+
unverified, and a wrong one can crash `backboardd` or lock the device, so they
|
|
447
|
+
return an error rather than a guess.
|
|
448
|
+
- Typing sends key positions, which iOS maps through the device's active
|
|
449
|
+
keyboard layout. Text that must be exact goes through the pasteboard, which
|
|
450
|
+
`sim_do` does by default.
|
|
284
451
|
- Region maps need a baseline inside the ~90 s history window. Older baselines
|
|
285
452
|
still get a reliable changed / did-not-change, without a map of what moved.
|
|
286
453
|
- Screen memory assumes a screen's layout is stable. A screen that reflows
|
|
@@ -290,9 +457,16 @@ simframe status / stop [--force] / devices / doctor
|
|
|
290
457
|
|
|
291
458
|
## Roadmap
|
|
292
459
|
|
|
293
|
-
- **
|
|
294
|
-
|
|
295
|
-
|
|
460
|
+
- **The accessibility tree without idb.** `AXPTranslator` would remove the last
|
|
461
|
+
heavyweight install. Capture, input and geometry already come from the daemon;
|
|
462
|
+
the tree is all that is left.
|
|
463
|
+
- **A compact state for the agent.** The element list, regions, what changed and
|
|
464
|
+
the last verdict, shaped so a model spends tokens on deciding rather than on
|
|
465
|
+
reading. This is where the token savings actually land.
|
|
466
|
+
- **Region bands from where elements cluster**, rather than fractions of screen
|
|
467
|
+
height. A date banner sitting above the tab bar was classified as a tab label
|
|
468
|
+
and its text entered a screen's identity, which would have expired at
|
|
469
|
+
midnight. Fixed for that case by a width rule; the underlying cause remains.
|
|
296
470
|
- **Reduce the input dependency.** idb is the one heavyweight requirement. Its
|
|
297
471
|
simulator input is a reimplementation of the Indigo HID transport rather than a
|
|
298
472
|
public API, so replacing it is real work, not a wrapper — but it is the last
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// swift-tools-version:5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "simframed",
|
|
6
|
+
platforms: [.macOS(.v13)],
|
|
7
|
+
targets: [
|
|
8
|
+
// Every private-framework call lives here, behind a protocol, so a
|
|
9
|
+
// signature change is a one-file fix and tests can run without a device.
|
|
10
|
+
.target(name: "PrivateAPI"),
|
|
11
|
+
// Platform-independent: hashing, downscaling, the on-disk frame layout.
|
|
12
|
+
.target(name: "SimframeCore", dependencies: ["PrivateAPI"]),
|
|
13
|
+
.executableTarget(name: "simframed", dependencies: ["SimframeCore", "PrivateAPI"]),
|
|
14
|
+
.testTarget(name: "SimframeCoreTests", dependencies: ["SimframeCore"]),
|
|
15
|
+
]
|
|
16
|
+
)
|