@mmerterden/dev-toolkit-mcp 2.24.1

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +708 -0
  2. package/LICENSE +21 -0
  3. package/README.md +387 -0
  4. package/index.js +1277 -0
  5. package/package.json +80 -0
  6. package/tools/design-check/content-cardinality.js +204 -0
  7. package/tools/design-check/geometry.js +140 -0
  8. package/tools/design-check/index.js +213 -0
  9. package/tools/design-check/mock-detect.js +213 -0
  10. package/tools/design-check/report.js +590 -0
  11. package/tools/design-check/scan.js +91 -0
  12. package/tools/design-check/scenario-inventory.js +598 -0
  13. package/tools/design-check/visual-compare.js +961 -0
  14. package/tools/ios-app-store-audit/context.js +180 -0
  15. package/tools/ios-app-store-audit/data/apple-required-sdks.json +32 -0
  16. package/tools/ios-app-store-audit/data/debug-tools-blocklist.json +133 -0
  17. package/tools/ios-app-store-audit/index.js +164 -0
  18. package/tools/ios-app-store-audit/models.js +47 -0
  19. package/tools/ios-app-store-audit/rules/asset-validation.js +72 -0
  20. package/tools/ios-app-store-audit/rules/binary-size.js +70 -0
  21. package/tools/ios-app-store-audit/rules/code-signing.js +95 -0
  22. package/tools/ios-app-store-audit/rules/dead-reference.js +131 -0
  23. package/tools/ios-app-store-audit/rules/debug-tool-leak.js +185 -0
  24. package/tools/ios-app-store-audit/rules/duplicate-resource.js +130 -0
  25. package/tools/ios-app-store-audit/rules/embedded-sdk.js +126 -0
  26. package/tools/ios-app-store-audit/rules/entitlement.js +105 -0
  27. package/tools/ios-app-store-audit/rules/extension-signing.js +105 -0
  28. package/tools/ios-app-store-audit/rules/info-plist.js +158 -0
  29. package/tools/ios-app-store-audit/rules/ipv6-compliance.js +101 -0
  30. package/tools/ios-app-store-audit/rules/privacy-manifest.js +121 -0
  31. package/tools/ios-app-store-audit/rules/production-hygiene.js +237 -0
  32. package/tools/ios-app-store-audit/rules/provisioning-profile.js +127 -0
  33. package/tools/ios-app-store-audit/rules/required-reason-api.js +123 -0
  34. package/tools/ios-app-store-audit/rules/sdk-floor.js +104 -0
  35. package/tools/ios-app-store-audit/rules/swift-abi.js +64 -0
  36. package/tools/ios-app-store-audit/rules/team-id.js +62 -0
  37. package/tools/ios-testflight/index.js +479 -0
  38. package/ui-tree-dumper.swift +122 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,708 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@mmerterden/dev-toolkit-mcp` are documented here.
4
+
5
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ Consumers pin minimum versions of this server (the multi-agent pipeline declares
8
+ per-tool minimums), so version discipline matters here:
9
+
10
+ - **Major** - a tool is removed or renamed, or an input schema stops accepting what it used to.
11
+ - **Minor** - a new tool, or a new optional input on an existing tool.
12
+ - **Patch** - fixes, docs, packaging, annotations, dependency bumps.
13
+
14
+ Releases before this file exists are recorded in the git tags and commit history.
15
+
16
+ ---
17
+
18
+ ## 2.24.1
19
+
20
+ ### Added
21
+
22
+ - Gate 13: Copilot CLI is now an exercised host, not a documented one. It was listed as
23
+ supported for months while the pipeline installer never registered this server there,
24
+ so every skill that reaches for a tool - `design-check`, the `ios_*` / `android_*`
25
+ simulator calls, the archive audits - had nothing to call on that host. The gate walks
26
+ the real round-trip: add, config landed, duplicate-add behaviour, remove.
27
+ - The duplicate-add assertion pins a behavioural difference the installer depends on:
28
+ `codex mcp add` replaces an existing entry and exits 0, while `copilot mcp add`
29
+ refuses and exits 1 with `already exists`. Treating that exit code as a failure made a
30
+ correct registration report as "skipped MCP registration (Command failed)".
31
+
32
+ ## 2.24.0
33
+
34
+ TestFlight pre-submission validation: ask Apple before Apple asks you.
35
+
36
+ ### Added
37
+
38
+ - **`ios_testflight_validate`** - wraps `xcrun altool --validate-app`. This is the
39
+ authoritative pre-submission gate and it is complementary to, not a replacement
40
+ for, `ios_app_store_audit`. The static audit reads the archive on disk, so it
41
+ structurally cannot know whether the bundle ID is registered, whether the
42
+ embedded profile matches the App Store Connect app record, whether this
43
+ version+build pair was already used, or whether an entitlement is actually
44
+ provisioned for the App ID. Only Apple's server answers those.
45
+ Returned ITMS codes are mapped onto the App Store rule each one implies
46
+ (`ITMS-90683` → purpose strings / 5.1.1, `ITMS-91053` → privacy manifest
47
+ required-reason, `ITMS-90046` → entitlement not permitted by the profile, ...),
48
+ with Apple's own message passed through verbatim for anything unmapped rather
49
+ than guessed at.
50
+ - **`ios_export_ipa`** - `.xcarchive` → signed `.ipa` via
51
+ `xcodebuild -exportArchive`, generating the `exportOptions.plist` from
52
+ arguments so callers do not hand-maintain one. A run that exits 0 without
53
+ producing an `.ipa` is reported as a failure, because xcodebuild does that.
54
+
55
+ ### Fixed
56
+
57
+ - **`design_scenario_inventory` produced a different answer for the same input.**
58
+ Target discovery walks the filesystem and directory read order is not stable
59
+ across runs, and three things derived their order from it: the `byKind` /
60
+ `byCost` count maps (insertion order, so `JSON.stringify` differed), which
61
+ relaunch claimed a shared in-app rider (first match won), and the order riders
62
+ were listed within a batch. The release gate went red roughly one run in three
63
+ with no code change - the shape of defect that trains you to re-run instead of
64
+ to look. Count maps are now emitted with sorted keys, relaunch iteration is
65
+ canonical, and rider ids are sorted within each batch.
66
+ - **Riders now ride with the relaunch that can actually drive their screen.**
67
+ Falling out of the fix above: two relaunch targets can match the same screen
68
+ while only one carries a launch argument, and the undrivable one could win the
69
+ riders purely by iterating first - yielding a plan that cannot be executed,
70
+ because the app never reaches the mock state those riders need. Assignment
71
+ scores an exact screen match above a substring one, then a relaunch with a
72
+ launch argument above one without, then canonical order. Substring pairing
73
+ ("Apis Form" ↔ "APIS") still works; it just no longer outranks an exact match.
74
+ - New `plan-determinism.test.mjs` permutes `buildPlan`'s input across 40 seeds
75
+ and asserts the plan is byte-identical. A pass-count is not proof of order
76
+ independence: this property test caught residual rider-ordering drift on its
77
+ first run, after 44 consecutive green runs of the existing suite had missed it.
78
+ - `scripts/gates.sh` no longer walks `.worktrees`. A git worktree is a separate
79
+ checkout of another branch, so its suites were being run against this tree and
80
+ gate 10c demanded that this tree's `npm test` name files that only exist on
81
+ another branch - a failure no change here could fix.
82
+
83
+ ### Notes
84
+
85
+ - **Three-tier auth, and a skipped gate never reads as a pass.** Tier 1 is an App
86
+ Store Connect API key; tier 2 is an Apple ID plus an app-specific password,
87
+ which matters because creating an API key needs an Admin or App Manager role
88
+ that many developers on a corporate team do not have, while any Apple ID holder
89
+ can generate an app-specific password. With neither, the tool returns
90
+ `verdict: "SKIPPED"` and the reason. "Apple did not object" and "Apple was
91
+ never asked" must not render the same way.
92
+ - **Secrets are referenced, never passed.** altool's `-p @keychain:<item>` and
93
+ `-p @env:<VAR>` indirections are the only forms used, so a password cannot
94
+ leak through the process list or through an error that echoes the command. A
95
+ unit test asserts no secret reaches argv.
96
+ - **A literal password is refused, not used.** Passing one would put the secret in
97
+ argv; doing it silently would be worse, because the caller would get no signal
98
+ that their credential was exposed. The error names the two safe forms and the
99
+ one-off `altool --store-password-in-keychain-item` command that creates the
100
+ keychain item.
101
+ - **A missing `password_env_var` is caught before altool sees it.** `-p @env:MISSING`
102
+ fails deep inside delivery with something that reads like a wrong password,
103
+ which sends you to rotate a credential that was fine.
104
+ - **`-allowProvisioningUpdates` is off by default** on `ios_export_ipa`. That flag
105
+ lets xcodebuild register devices and create or modify provisioning profiles in
106
+ the developer account. A tool whose job is to check a build must not silently
107
+ alter the account the build belongs to, so it is opt-in via
108
+ `allow_provisioning_updates`.
109
+ - Neither tool is in `READ_ONLY_TOOLS`: `ios_export_ipa` writes files and
110
+ `--validate-app` uploads the binary to Apple, so both require approval rather
111
+ than running unattended.
112
+ - The generated `exportOptions.plist` temp dir is removed after a successful
113
+ export and kept (and reported) after a failure, so a failed run stays
114
+ inspectable without accumulating one temp dir per export forever.
115
+ - Tool count 78 → 80 (iOS 35 → 37, Store Compliance 3 → 5).
116
+
117
+ ## 2.23.0
118
+
119
+ Documentation that claimed more than it checked, and the gate that let it.
120
+
121
+ **Editor support is now stated as verified vs compatible.** The README opened with
122
+ "Works with Claude Code, Copilot CLI, Cursor, Antigravity, and VS Code Copilot Chat
123
+ - the multi-agent-pipeline's full-orchestration targets". That parenthetical stopped
124
+ being true in pipeline v10.7.0, when the Cursor / Antigravity / Codex / Copilot Chat
125
+ adapters were deleted; the pipeline has targeted Claude Code and Copilot CLI only
126
+ ever since.
127
+
128
+ The list is not simply wrong, though: this is a plain stdio MCP server, so it does
129
+ run in any MCP client, and the config snippets for those clients stay. What was
130
+ wrong was presenting five hosts as supported when the gates exercise two. That
131
+ distinction has a price tag now: 2.20.0 and 2.21.0 shipped an illegal `outputSchema`
132
+ on the first tool, so Claude Code rejected the entire `tools/list` and served zero
133
+ of the 78 - in the primary host, for two releases, with every gate green.
134
+
135
+ **Gate 3b: per-family counts, not just the headline.** Gate 3 checked the two
136
+ headline numbers and nothing else, so every subcount drifted behind a green check:
137
+
138
+ | README said | reality |
139
+ |---|---|
140
+ | Device Control (42 tools) | 58 |
141
+ | Store Compliance (4 tools) | 3 |
142
+ | iOS Tools (34), diagram (31) | 35 |
143
+ | 5 categories summing to 57 | 78 |
144
+
145
+ The last row is the interesting one: the whole `design_*` family, six tools, was
146
+ missing from the category list entirely, which is why the listed categories could
147
+ never sum to the advertised total. A gate that verifies one number while its
148
+ components drift reports an agreement it never checked.
149
+
150
+ 3b matches each `### <Family> Tools (N)` section against the served family count
151
+ and requires the category bullets to sum to the total, so an unlisted family fails
152
+ instead of hiding in the arithmetic. Verified by re-injecting a wrong subcount.
153
+
154
+ All counts corrected, and the Design Audit category added.
155
+
156
+ ## 2.22.0
157
+
158
+ **Fixes a server that Claude Code could not load at all.** `ios_list_devices`
159
+ declared `outputSchema: { type: "array", items: {...} }`. MCP's `structuredContent`
160
+ is an object, so the schema describing it must be `type: "object"`, and the host
161
+ validates the ENTIRE `tools/list` response - so one illegal entry on the FIRST tool
162
+ made all 78 unavailable:
163
+
164
+ ```
165
+ Reconnected to dev-toolkit, but fetching tools failed:
166
+ [{ code: invalid_value, path: [tools, 0, outputSchema, type],
167
+ message: Invalid input: expected "object" }]
168
+ ```
169
+
170
+ `/mcp` showed `dev-toolkit - connected - tools fetch failed`: a connected server
171
+ with zero usable tools. Any pipeline command depending on it (`design-check`,
172
+ `test`, `build-optimize`) halted at its MCP gate.
173
+
174
+ The declaration is removed rather than reshaped. Wrapping the payload as
175
+ `{ devices: [...] }` would have made it legal, but this tool's text output is a
176
+ JSON array that callers parse, so re-shaping it is a breaking change bought for
177
+ structured output nobody requested. Dropping the schema restores a legal
178
+ `tools/list` and leaves the text output byte-identical.
179
+
180
+ **Why no gate caught it.** Gate 9 hand-wrote what it believed the spec allowed:
181
+
182
+ ```js
183
+ if (t.outputSchema.type !== "object" && t.outputSchema.type !== "array")
184
+ ```
185
+
186
+ It explicitly permitted `array`. The gate encoded a wrong belief about the spec and
187
+ then verified the code against that belief, so the code and the gate agreed with
188
+ each other and both disagreed with the client. Gate 9 is now object-only, and new
189
+ **gate 9b** stops asserting beliefs altogether: it pipes the real `tools/list`
190
+ response through `ListToolsResultSchema` from `@modelcontextprotocol/sdk` - the same
191
+ Zod schema the host uses - so the check is authoritative rather than a guess.
192
+ Verified by re-injecting the bad schema: 9b reproduces the host's error verbatim,
193
+ down to the `path` array.
194
+
195
+ ## 2.21.0
196
+
197
+ **Fixture-count noise is demoted, not reported as defects** (`docs/design-check-gaps.md`
198
+ section 1, the item that dominated a real 109-target audit). A live capture is
199
+ compared against a screen frame, and a screen frame carries whatever the mock
200
+ fixture produced: 5 passenger rows in the design against 3 in the app makes every
201
+ container height and every downstream position differ for a reason that is not a
202
+ defect.
203
+
204
+ `tools/design-check/content-cardinality.js` detects a repeated-group count
205
+ difference from the element lists and demotes the geometry findings it explains.
206
+ On by default; `content_cardinality: false` returns the raw findings.
207
+
208
+ Written defensively, because the dangerous direction here is not noise but a
209
+ HIDDEN DEFECT - demote too eagerly and the engine silently stops reporting real
210
+ deviations, invisibly, in the one report whose job is to surface them. Five rules,
211
+ each an explicit test:
212
+
213
+ - nothing is ever DELETED; an affected finding stays with `advisory: true` plus a
214
+ shared `rootCause`, so it renders under the existing "DO NOT CHANGE" group, and
215
+ `contentCardinality.demoted` reports the count - a demotion is never silent;
216
+ - only geometry findings are eligible. Copy, colour, typography, font-family,
217
+ tap-target and missing-element findings are never touched: a row-count
218
+ difference cannot make a colour wrong or a string mistranslated, so it can never
219
+ be the explanation for one;
220
+ - an absent group is NOT a count difference. 1-vs-0 is a missing element, which is
221
+ a real defect and exactly what this must not explain away;
222
+ - only findings AT OR BELOW the first differing group. A count difference shifts
223
+ what follows it, never what precedes it. A finding whose position cannot be
224
+ established keeps full severity - demoting on a guess is the failure mode this
225
+ module must not have;
226
+ - idempotent: an already-advisory or `verified` finding is left byte-identical.
227
+
228
+ The 21 tests cover both directions, with more weight on "must NOT demote" than on
229
+ "must demote". 8 further tests assert the WIRING (the module is imported, the
230
+ detector is called, the demotion is applied after copy comparison so it sees every
231
+ finding, and the opt-out reaches it from the tool args) - a unit-tested module that
232
+ nothing calls is the defect class the sibling pipeline repo just spent a release
233
+ removing.
234
+
235
+ **FIELD VALIDATION PENDING.** The gaps-doc acceptance criterion ("a screen whose
236
+ fixture returns a different row count produces zero geometry findings") needs the
237
+ real module, a live capture and Figma design context to confirm end to end. What
238
+ is proven is the safe direction. Treat the tuning constants as provisional until a
239
+ real run is measured, and check `contentCardinality.demoted` against the 52
240
+ confirmed deviations that audit found before trusting a quieter report.
241
+
242
+ ## 2.20.0
243
+
244
+ **`ios_app_store_audit` has tests.** The 18-rule App Store compliance scanner
245
+ shipped 2,507 lines of rule code with none, while `npm test` ran only the
246
+ design-check suite and gates step 10 globbed for `*.test.mjs`, found exactly one,
247
+ and honestly reported "1 suite passed". An empty `__tests__/fixtures/` directory
248
+ sat beside the rules as evidence that tests had been intended. This is the code
249
+ that decides whether an archive is fit to upload, so a false PASS costs a
250
+ rejection round.
251
+
252
+ 74 tests now cover the dispatcher contract (rule selection, `core` as a strict
253
+ subset of `all`, an unknown rule id selecting nothing rather than falling back to
254
+ the full scan, severity ordering, summary/verdict consistency, and the promise
255
+ that one throwing rule cannot abort the scan) plus per-rule logic against
256
+ synthetic contexts. Three properties are asserted across every rule, each
257
+ protecting against a silent-miss shape the dispatcher permits:
258
+
259
+ - a rule must return an ARRAY - `runAudit` does `if (Array.isArray(out))` and
260
+ otherwise discards the result while still listing the rule in `rulesRun`, so a
261
+ rule returning a bare object reports nothing and looks like a clean pass;
262
+ - a rule must not throw on a sparse context (empty/null `infoPlist`, null
263
+ collections, no executable) - `runAudit` converts a throw into a "tool bug"
264
+ warning that REPLACES whatever that rule would have found;
265
+ - every violation must carry the rule's own `ruleID`, or it is unattributable and
266
+ breaks the documented alphabetical-within-severity sort.
267
+
268
+ **Gates 10b and 10c.** 10b requires every `tools/<dir>/` to carry at least one
269
+ suite, so an untested tool directory is visible instead of being implied as
270
+ covered. 10c requires `npm test` to name every suite on disk: Node 25 rejects
271
+ `node --test tools/` (it resolves the directory as a module), so the script lists
272
+ suites explicitly, and that list would otherwise go stale and hand a contributor
273
+ a false green.
274
+
275
+ **`android_set_locale` verifies instead of asserting.** It already read the locale
276
+ back with `get-app-locales`, but reported the REQUESTED value as fact and put the
277
+ device's answer in a parenthetical. A device that accepted the command and applied
278
+ a different locale, or none, still read as success. It now compares them
279
+ (case-insensitive, and accepting a more specific tag than asked for, so
280
+ `tr` -> `tr-TR` is a match) and returns an error on mismatch.
281
+
282
+ **Dependency audit is an allowlist, not a severity floor.** CI ran
283
+ `npm audit --omit=dev --audit-level=high`. That was right about the two live
284
+ findings and wrong as a policy: it ignores every moderate advisory forever,
285
+ including unrelated future ones that could well be reachable here. The
286
+ justification also lived only in a workflow comment, where nothing could
287
+ invalidate it.
288
+
289
+ `scripts/audit-allowlist.mjs` names each accepted advisory with its reason and
290
+ re-verifies that reason on every run. The current exception is
291
+ `@hono/node-server < 2.0.5` (GHSA-frvp-7c67-39w9), reached transitively through
292
+ `@modelcontextprotocol/sdk`, which pins `^1.19.9`: a path traversal in
293
+ `serve-static` on Windows. The reason it does not apply here is that this server
294
+ is stdio-only, so the gate checks that no shipped source imports an HTTP
295
+ transport, hono, or express - if someone adds a streamable-HTTP transport, the
296
+ exception stops applying on that same run. There is no SDK release that lifts the
297
+ pin (1.29.0 is the latest and still depends on `^1.19.9`), and
298
+ `npm audit fix --force` would downgrade the SDK to 1.24.3. Anything unlisted now
299
+ fails at any severity, an accepted advisory that escalates fails, and a stale
300
+ entry whose advisory is gone fails too.
301
+
302
+ **Stale rule count.** `index.js` and `context.js` said "17-rule" / "17 rules"
303
+ while the registry held 18; the tool descriptor the model reads to choose
304
+ `--rules` said 17 too. Corrected, and two tests now tie the descriptor's
305
+ advertised full-scan and `core` counts to the rules that actually execute.
306
+
307
+ ## 2.19.0
308
+
309
+ First three items off the design-check handoff spec (`docs/design-check-gaps.md`).
310
+
311
+ **section 8 - `design_scenario_inventory` summary mode.** The full payload measured
312
+ 71,196 characters for a 109-target module and exceeded the host's tool-result cap
313
+ on all seven runs of a real audit, forcing a write-to-file round trip every time.
314
+ `summary: true` drops `targets[]`, which is nearly all of the bulk (label, screen,
315
+ driver and a file+line+snippet evidence block per target). Nothing structural is
316
+ lost: `plan[].targetIds` already carries every id in the order it should be driven
317
+ and `groups[].ids` carries the per-screen lists, so a run can be driven entirely
318
+ from the summary and ask for labels and evidence later. `targetsOmitted` and
319
+ `summary: true` are stated explicitly, so a consumer can tell a withheld list from
320
+ a scan that found nothing. Measured on the real module's shape at its reported
321
+ target count: 66,591 characters down to 450.
322
+
323
+ **section 9, second half - `ios_screenshot` gains `path`.** Returning a base64 PNG
324
+ per capture exhausts the caller's context across the 100+ captures an audit takes,
325
+ which is why the motivating run shelled out to `xcrun simctl io` directly. With
326
+ `path` the file is written and only its location comes back. A missing parent
327
+ directory is an error rather than a silent miss.
328
+
329
+ **section 9, first half - the AX-box caveat is now on the tool.**
330
+ `design_ui_geometry` returns accessibility boxes, glyph runs and hit areas, not
331
+ layout containers, so a 247pt design text container measured against its 78pt glyph
332
+ box produces phantom findings. The description says so and points at measuring from
333
+ the pixels instead. Documented rather than changed: returning container-union boxes
334
+ is a real option but it changes what every existing caller receives.
335
+
336
+ Still open from the spec: sections 1, 2, 2a, 3, 4, 5, 6, 6a (component-variant
337
+ comparison, variant previews, per-finding fileKey, typography/colour/copy
338
+ expectations, code location and token snapping) and section 7 (scroll-stitch).
339
+
340
+ ## 2.18.0
341
+
342
+ **A tool name with a valid family prefix but no matching case reported success.**
343
+ Every handler's `default:` arm returns null, and `dispatchStep` / the CallTool
344
+ handler only threw "Unknown tool" for a name matching no prefix at all. So
345
+ `ios_taap`, `design_reprot`, `android_tpa` reached their handler, came back
346
+ null, and were answered to the host as the literal text `"null"` with no
347
+ `isError`. Inside `agent_run_steps` the step was recorded `status: "ok"` and the
348
+ verdict stayed `all_ok`. Same false-success class as the 2.13.0 batch fix, in a
349
+ different place. A null return is now an error at both levels.
350
+
351
+ **outputSchema + structuredContent (spec 2026-07-28).** Nine tools declare an
352
+ output schema and fill `structuredContent` alongside their existing text
353
+ content: the two accessibility audits, the archive and APK audits,
354
+ `ios_list_devices`, `ios_visual_diff`, `android_launch_time`,
355
+ `design_ui_geometry` and `agent_run_steps`. Only shapes read off the return
356
+ statement are declared - `ios_xcresult` (four shapes by mode),
357
+ `ios_app_store_audit`, the delegated `design_*` reports and `web_eval` are
358
+ deliberately left out, because a schema that does not match its payload makes
359
+ the host reject a good result. Text content is unchanged, so a host that
360
+ ignores `structuredContent` sees no difference.
361
+
362
+ **Two new gates.** Gate 8 drives three unknown names through the call and batch
363
+ paths and requires an error on each. Gate 9 requires every tool advertising an
364
+ outputSchema to actually return `structuredContent`, and to keep its text
365
+ content beside it. 14 gates, all green.
366
+
367
+ ## [2.17.0] - 2026-07-26
368
+
369
+ The audit measured geometry only, so it could not answer whether the copy was
370
+ right — and a perceptual diff reporting "these pixels differ" is not an answer.
371
+
372
+ ### Added
373
+
374
+ - **`compareCopy` — copy checked against the design's UX-WRITING ANNOTATION.** The
375
+ authoritative strings are the annotation on the Figma node, not the frame render,
376
+ which is a stale picture of the words. An exact live match is reported as
377
+ verified (so the report shows what was checked, not only what failed), a near
378
+ miss as a copy defect carrying both strings, and a string with no counterpart as
379
+ advisory, because the capture may not show the state that carries it.
380
+
381
+ Matching is one-to-one: without claiming, a single live label is offered as the
382
+ near match for several unrelated design strings and a loose threshold turns "not
383
+ on this screen" into a fabricated defect. Exact matches claim first, then near
384
+ matches take what remains, strongest pair first.
385
+
386
+ Reachable from `design_visual_compare` via `design_copy`, so one call covers
387
+ geometry and copy together.
388
+
389
+ Found on the audited module the first time it ran: the design specifies "Soyadı",
390
+ the app renders "Soyad" — with 10 of 13 strings confirmed on-spec.
391
+
392
+ ## [2.16.0] - 2026-07-26
393
+
394
+ A stretching element's horizontal insets were measured against the frame, which
395
+ reported a perfectly on-spec button as off.
396
+
397
+ ### Fixed
398
+
399
+ - **Horizontal insets are measured against the parent, not the frame.** A button
400
+ 16pt inside a modal keeps 16pt on any device, but its distance to the frame edge
401
+ changes with both the screen width and the modal's own width. Measured against
402
+ the frame, design 32pt vs live 40pt looked like a defect; against the parent both
403
+ sides read 16/16 — identical. Parenthood is recovered geometrically (the smallest
404
+ box that fully contains this one), so it needs no project knowledge and applies
405
+ to either side. A genuinely wrong inset inside a parent is still reported.
406
+
407
+ ### Changed
408
+
409
+ - **Advisories collapse to one line.** They exist to say "do not act on this", so
410
+ a full card per advisory beside the real findings doubled the page for the same
411
+ elements while giving the reader nothing to do. Now a single expandable summary.
412
+
413
+ ## [2.15.0] - 2026-07-26
414
+
415
+ The checks added in 2.12–2.14 overlapped, so one defect could arrive in the
416
+ report three times and a finding the engine could not actually confirm was
417
+ presented with the same weight as one it could.
418
+
419
+ ### Changed
420
+
421
+ - **One measurement, one owner.** `edge` was reporting distances that `inset` and
422
+ `gap` already owned — including, via the raw spec geometry, the very
423
+ wrapper-vs-visible phantom 2.11.0 removed. It is now restricted to the one
424
+ thing nothing else measures: the horizontal distance between two siblings (an
425
+ icon-to-label gap). Vertical sibling distances belong to `gap`, horizontal
426
+ frame distances to `inset`, which resolves them from the pixels.
427
+ - **Centring and symmetry read the pixel edges too.** They were computed from the
428
+ raw boxes, so a full-bleed wrapper was trivially "centred" while the live AX
429
+ union was not — an off-centre finding on a correct screen. The pixel edges are
430
+ now resolved once per element and every check reads the same answer.
431
+ - **Unconfirmed findings are advisory.** An inset with no pixel confirmation may
432
+ be comparing a wrapper box against a glyph run, so it is reported with that
433
+ reason attached rather than as a defect. Alignment findings derived from the
434
+ same boxes follow it.
435
+
436
+ Re-measuring an existing 28-screen audit: 109 reported deviations became 52
437
+ confirmed ones, and the worst offender went from 7 findings to 1.
438
+
439
+ ## [2.14.0] - 2026-07-26
440
+
441
+ Design conformance was measured with absolute positions, which answer the wrong
442
+ question: two screens showing the same component with different mock content put
443
+ it at different Y, so a conformant screen was reported as "+37pt off" while a
444
+ real defect beside it went unreported.
445
+
446
+ ### Added
447
+
448
+ - **Local edge distances — the on-device-inspector measurement, made generic.**
449
+ For every element, the four distances to whatever actually bounds it: the
450
+ nearest sibling overlapping on the perpendicular axis, else the frame edge.
451
+ The identical pure-geometry function runs over the design box list and the live
452
+ box list, so no per-project knowledge and nothing that has to execute inside
453
+ the app. A block pushed down by a taller fixture keeps its own distances and
454
+ stays silent; a padding or gap that genuinely changed surfaces on the exact
455
+ edge, naming what it was measured against.
456
+
457
+ Three cases are demoted to advisory rather than reported as defects, because
458
+ each is the same fact arriving twice or a quantity that is not local:
459
+ - a trailing edge that moved only because the element itself grew (already
460
+ reported as size),
461
+ - a distance measured to a sibling on one side and to the frame on the other,
462
+ - a vertical distance to the frame, which accumulates every block above it and
463
+ so has exactly the flaw absolute Y had. Horizontal frame distances are real
464
+ margins and stay real.
465
+
466
+ ### Notes
467
+
468
+ - Font size, weight and colour still cannot be read from outside the process, so
469
+ typography remains a design-side expectation to verify in code rather than a
470
+ measured delta.
471
+
472
+ ## [2.13.0] - 2026-07-26
473
+
474
+ `agent_run_steps` reported failed steps as successes. A batch could run to
475
+ completion with `verdict: "all_ok"` while every step in it had failed.
476
+
477
+ ### Fixed
478
+
479
+ - **Step failures are now reported as failures.** `run()` signals a command
480
+ failure by RETURNING a string prefixed with `ERROR: ` rather than by throwing.
481
+ The `tools/call` handler tests for that marker with `isFailure()` before
482
+ answering the host, but the batch loop only had a `try`/`catch`, so a broken
483
+ step was recorded `status: "ok"`, `errors` stayed `0`, the verdict published
484
+ `all_ok`, and `stop_on_first_error` never fired because nothing threw. The loop
485
+ now applies the same `isFailure()` judgement the single-call path does.
486
+ - **`stop_on_first_error` stops the batch.** Previously only a thrown exception
487
+ could halt it, which meant the common case (a CLI exiting non-zero) ran every
488
+ remaining step against a device left in an unexpected state.
489
+
490
+ ### Added
491
+
492
+ - **`aborted` in the batch report**, distinguishing "stopped early because a step
493
+ failed" from "ran everything and some steps failed". An aborted batch also sets
494
+ `isError` on the result envelope; failures the caller opted into with
495
+ `continue_on_error` do not, since there the call did what it was asked.
496
+ - **`design_*` steps are dispatched.** The tool description offered "any tool
497
+ name" while the loop only handled `ios_*`, `android_*` and `web_*`, so the six
498
+ design-check tools were refused with "Unknown tool".
499
+ - **Gate 7 in `scripts/gates.sh`** covers all of the above, probing with
500
+ `ios_archive_audit` on a missing path: it returns the failure marker without
501
+ throwing and needs no simulator, emulator or external binary, so it exercises
502
+ the same path on every runner. Verified to fail against the old behaviour.
503
+ - **Gate 9 in `scripts/gates.sh`** requires a CHANGELOG entry for the version in
504
+ `package.json`. Added because 2.11.0 and 2.12.0 shipped without one.
505
+
506
+ ### Changed
507
+
508
+ - The `agent_run_steps` description and its `tool` schema text now state the
509
+ accepted prefixes and the failure semantics instead of "any tool name".
510
+ - Nested `agent_*` steps are refused explicitly rather than falling through to
511
+ "Unknown tool"; a batch able to nest itself has no recursion bound.
512
+
513
+ ---
514
+
515
+ ## [2.12.0] - 2026-07-26
516
+
517
+ Recorded after the fact: this release shipped without a CHANGELOG entry. Summary
518
+ transcribed from commit `a8d83ba`.
519
+
520
+ `design_visual_compare` answers "is it built 1:1" with relations rather than
521
+ absolute positions.
522
+
523
+ ### Changed
524
+
525
+ - **Absolute Y was the wrong unit.** Two screens rendering the same component
526
+ with different mock content place it at a different Y, so the audit flagged
527
+ conformant screens as "+37pt off" while missing an actual defect beside it.
528
+ Absolute top offset is demoted to advisory; the sibling gap plus the relational
529
+ checks carry the vertical signal.
530
+
531
+ ### Added
532
+
533
+ - Content-independent checks promoted to first class: centred-in-frame (offset
534
+ from centre, not position), margin symmetry, shared centre line, and
535
+ icon/control size, which also flags anything under the 44pt minimum tap target.
536
+ - 7 tests (80 total).
537
+
538
+ ---
539
+
540
+ ## [2.11.0] - 2026-07-25
541
+
542
+ Recorded after the fact: this release shipped without a CHANGELOG entry. Summary
543
+ transcribed from commit `0ec8ba3`.
544
+
545
+ ### Fixed
546
+
547
+ - **False-positive inset findings.** A Figma node tree reports the instance
548
+ wrapper (full-bleed, x=0, w=375) while the accessibility tree reports the
549
+ visible card inside it (inset 16pt), so comparing one against the other
550
+ reported "+16pt off" on a conformant screen. For a row spanning at least 80% of
551
+ the frame the insets are now read off the two renders instead. Verified on a
552
+ real screen: 7 deviations down to 3, all 3 genuine. A genuinely wrong inset is
553
+ still caught, with a regression test.
554
+ - An advisory-only screen no longer prints an empty DEVIATIONS heading, and that
555
+ guard's assertion targets the status chip rather than matching a substring
556
+ anywhere in the document.
557
+
558
+ ### Added
559
+
560
+ - **Fixed-field fix prompt** replacing prose that lost which property moved and
561
+ by how much: element | property | expected -> actual | delta, plus a SHARED
562
+ ROOT CAUSE section collapsing a delta repeated across elements into one likely
563
+ container or token cause, and a DO NOT CHANGE section so advisories and fixture
564
+ differences are not "fixed" into new bugs.
565
+ - 6 tests (73 total).
566
+
567
+ ---
568
+
569
+ ## [2.10.0] - 2026-07-25
570
+
571
+ `design_visual_compare` now measures what a designer actually specifies, in
572
+ points, instead of comparing raw pixel dimensions between two frames that are
573
+ rarely the same size.
574
+
575
+ ### Changed
576
+
577
+ - **Responsive, point-based conformance measurement.** Raw width/height deltas
578
+ were misleading: a device and a Figma frame are seldom the same width (402pt
579
+ vs 375pt), so a full-bleed 375pt design container measured against a 402pt
580
+ screen's 16pt-inset card reported "-29px too narrow" when nothing was wrong,
581
+ and any container that hugs its content turned every fixture difference into a
582
+ size finding that buried the real defects. The comparison now reports
583
+ per-element edge insets (left/right) rather than raw width, gaps between
584
+ consecutive elements, vertical placement, and font size, family and text
585
+ colour sampled from the text ink rather than the box average - the box average
586
+ previously returned the background colour and so passed every check. Pass
587
+ `responsive: false` to restore the previous absolute-delta behaviour.
588
+ - **Height is advisory.** It is still reported, but a content-driven height
589
+ difference no longer decides pass/fail and no longer inflates the deviation
590
+ count. Advisories render in their own uncounted group in the report.
591
+
592
+ ### Fixed
593
+
594
+ - **The report dropped the measured delta.** `findingLine` discarded `f.detail`
595
+ whenever `expected`/`actual` were present, so an inset finding rendered as
596
+ "left 0pt -> left 16pt" and lost both the delta and which edge moved. The
597
+ detail is the actionable half of a responsive measurement, so it now renders
598
+ alongside.
599
+ - **An advisory-only screen contradicted its own header.** The header total
600
+ excluded advisories but the per-screen status label and `fail` class were
601
+ computed by a separate expression that still counted them, so a screen whose
602
+ only finding was advisory rendered as DEVIATION underneath a header that said
603
+ 0 deviations. Both now exclude advisories on the same terms.
604
+
605
+ Adds 13 tests pinning the inset/gap/advisory behaviour and the report's
606
+ advisory accounting.
607
+
608
+ ## [2.9.0] - 2026-07-25
609
+
610
+ Every item below is one shape of defect: a tool that reported success while
611
+ doing nothing. Three wrapped host-tool flags had silently stopped existing, and
612
+ because no failure ever carried `isError`, no host could tell.
613
+
614
+ ### Fixed
615
+
616
+ - **`isError` on every failure.** The `CallTool` dispatch returned command
617
+ failures as ordinary text, so a host - and the pipeline gates reading these
618
+ results - saw failure as success. Failures now return `isError: true`. Failing
619
+ command output is also capped at 600 chars: a failed `simctl` call was
620
+ inlining its entire ~3 KB usage page into the caller's context.
621
+ - **`ios_biometric` was a no-op that claimed success.** `simctl keychain
622
+ <device> biometric-enroll` / `biometric-match` do not exist - `keychain`
623
+ supports only `add-root-cert`, `add-cert`, `reset`. Now drives the BiometricKit
624
+ notification via `notifyutil` inside the simulator, and because `notifyutil`
625
+ exits 0 even when it cannot post the name, its output is inspected: a "Failed
626
+ with code N" line is reported as a failure with next steps instead of
627
+ "success simulated".
628
+ - **`ios_go_home` was a no-op.** `simctl io <device> pressButton` does not exist
629
+ - `io` supports only `enumerate`, `poll`, `recordVideo`, `screenshot`,
630
+ `screenConfig`. Routed through `idb ui button HOME`, like tap/swipe/type.
631
+ - **`android_set_locale` reported success unconditionally.** It broadcast the
632
+ dead pre-Android-7 `SET_LOCALE` intent with stderr sent to `/dev/null`, and
633
+ `am broadcast` exits 0 even when nothing handles the intent. Now uses the
634
+ supported per-app path `cmd locale set-app-locales` (API 33+), inspects the
635
+ output for `Unknown command` / usage text before claiming success, and reads
636
+ the value back. `package_name` stays optional in the schema (no breaking
637
+ change) but the call returns an error instead of doing nothing without it.
638
+ - **Command injection.** Commands are built as strings for a shell, and `$( )`
639
+ expands inside double quotes, so double-quoting a caller value did not contain
640
+ it. `ios_revoke_permission`'s `service` and the Android `permission` names were
641
+ interpolated with no quoting at all. Adds `shq()` (POSIX single-quoting),
642
+ `num()` for coordinates and scales, and `token()` for enum-shaped values, and
643
+ applies them across every caller-supplied interpolation.
644
+ - **Unavailable-capability guards** ("Xcode not installed", "Android SDK not
645
+ installed") returned as success text; they are failures and now say so.
646
+ - **ITMS-91061 filed at the wrong severity.** A required-SDK framework missing
647
+ `PrivacyInfo.xcprivacy` was a `5.1.1` WARNING; it has been an enforced upload
648
+ rejection since 2025-02-12. Now `ITMS-91061` at ERROR. This changes
649
+ archive-guard verdicts: archives that previously passed with a warning fail now.
650
+
651
+ ### Added
652
+
653
+ - **`sdk-floor` audit rule** (ITMS-90725), bringing `ios_app_store_audit` to
654
+ **18 rules**. Asserts `DTSDKName` / `DTPlatformVersion` major >= 26 and
655
+ `DTXcode` >= 2600, the iOS 26 / Xcode 26 floor in force since 2026-04-28 - a
656
+ hard upload rejection that nothing checked. Grouped as `core` because it costs
657
+ nothing to run. Reports a WARNING when an archive carries no build receipts at
658
+ all, rather than passing silently.
659
+
660
+ ### Changed
661
+
662
+ - **`peerDependencies.playwright`** from the exact pin `1.61.1` to
663
+ `>=1.60.0 <2`. Since npm 7 an unsatisfiable peer is a hard `ERESOLVE`, so the
664
+ exact pin broke installs for anyone already on 1.62.0. None of the APIs removed
665
+ in the 1.60 window are used here, and browser-binary coupling is already
666
+ enforced inside the playwright package itself.
667
+ - **`engines.node`** from `>=18.0.0` to `>=20.0.0`, matching what
668
+ `playwright@1.62.0` requires and what CI already runs.
669
+
670
+ ## [2.7.1] - 2026-07-25
671
+
672
+ ### Added
673
+
674
+ - **Tool annotations** on all 78 tools (MCP `2025-11-25`): `readOnlyHint`,
675
+ `destructiveHint`, `idempotentHint`, `openWorldHint`. Hosts use these for
676
+ permission prompts and for deciding what may run unattended. 24 tools are
677
+ read-only (screenshots, UI trees, audits, device/app listings), 5 are
678
+ destructive (`ios_erase_device`, `ios_keychain_reset`, `ios_reset_permissions`,
679
+ `android_uninstall_app`, `android_clear_app_data`), 8 are open-world (the
680
+ `web_*` family). The classification lives in one auditable block in
681
+ `index.js`, not inline on 78 literals.
682
+ - **`scripts/gates.sh`** - the single definition of "safe to ship": syntax over
683
+ every loaded file, a stdio `tools/list` handshake that must answer with a
684
+ non-zero count, the tool count matching every place it is advertised
685
+ (`package.json` description + README header), `npm pack --dry-run` covering
686
+ every runtime `tools/*/` directory, stdout hygiene (stdout is the JSON-RPC
687
+ channel), version single-sourcing, and every test suite in the repo. Run with
688
+ `npm run gates`.
689
+ - **CI** (`.github/workflows/ci.yml`) on ubuntu + macOS x Node 20/22, running
690
+ `npm run gates` so the release path and CI cannot drift apart.
691
+ - **`npm run gates`** script.
692
+
693
+ ### Changed
694
+
695
+ - `@modelcontextprotocol/sdk` range `^1.0.0` -> `^1.29.0`. The old range let a
696
+ four-major spread install under the same declaration; the tested version is
697
+ now the declared one. Transitive advisories dropped from 8 (2 high: `hono`,
698
+ `fast-uri`) to 2 moderate.
699
+ - `serverInfo.version` is read from `package.json` instead of a second
700
+ hardcoded literal, so a version bump can no longer leave the server
701
+ advertising a version consumers gate on but never received.
702
+
703
+ ### Known
704
+
705
+ - Two moderate advisories remain in `@hono/node-server`, pulled in by the SDK's
706
+ streamable-HTTP transport. This server only runs over stdio and never loads
707
+ that path; the fix has to land upstream in the SDK (npm's only local
708
+ "fix" is a downgrade to an older SDK, which would be a regression).