@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.
- package/CHANGELOG.md +708 -0
- package/LICENSE +21 -0
- package/README.md +387 -0
- package/index.js +1277 -0
- package/package.json +80 -0
- package/tools/design-check/content-cardinality.js +204 -0
- package/tools/design-check/geometry.js +140 -0
- package/tools/design-check/index.js +213 -0
- package/tools/design-check/mock-detect.js +213 -0
- package/tools/design-check/report.js +590 -0
- package/tools/design-check/scan.js +91 -0
- package/tools/design-check/scenario-inventory.js +598 -0
- package/tools/design-check/visual-compare.js +961 -0
- package/tools/ios-app-store-audit/context.js +180 -0
- package/tools/ios-app-store-audit/data/apple-required-sdks.json +32 -0
- package/tools/ios-app-store-audit/data/debug-tools-blocklist.json +133 -0
- package/tools/ios-app-store-audit/index.js +164 -0
- package/tools/ios-app-store-audit/models.js +47 -0
- package/tools/ios-app-store-audit/rules/asset-validation.js +72 -0
- package/tools/ios-app-store-audit/rules/binary-size.js +70 -0
- package/tools/ios-app-store-audit/rules/code-signing.js +95 -0
- package/tools/ios-app-store-audit/rules/dead-reference.js +131 -0
- package/tools/ios-app-store-audit/rules/debug-tool-leak.js +185 -0
- package/tools/ios-app-store-audit/rules/duplicate-resource.js +130 -0
- package/tools/ios-app-store-audit/rules/embedded-sdk.js +126 -0
- package/tools/ios-app-store-audit/rules/entitlement.js +105 -0
- package/tools/ios-app-store-audit/rules/extension-signing.js +105 -0
- package/tools/ios-app-store-audit/rules/info-plist.js +158 -0
- package/tools/ios-app-store-audit/rules/ipv6-compliance.js +101 -0
- package/tools/ios-app-store-audit/rules/privacy-manifest.js +121 -0
- package/tools/ios-app-store-audit/rules/production-hygiene.js +237 -0
- package/tools/ios-app-store-audit/rules/provisioning-profile.js +127 -0
- package/tools/ios-app-store-audit/rules/required-reason-api.js +123 -0
- package/tools/ios-app-store-audit/rules/sdk-floor.js +104 -0
- package/tools/ios-app-store-audit/rules/swift-abi.js +64 -0
- package/tools/ios-app-store-audit/rules/team-id.js +62 -0
- package/tools/ios-testflight/index.js +479 -0
- 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).
|