staysfixed 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,82 @@ numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
9
  Nothing yet.
10
10
 
11
+ ## [0.12.0] — 2026-08-31
12
+
13
+ Five capabilities that were missing rather than broken. Each was built by a lane working on
14
+ its own, each had to prove itself against a real product before it was believed, and three of
15
+ the five turned up a false all-clear on the way in.
16
+
17
+ ### Record yourself using it once, and it is checked for ever
18
+
19
+ Everything this tool checked before, it worked out by READING — routes out of the source,
20
+ exports out of a package, screens out of a router. That finds what the code says it does. It
21
+ never found the way somebody actually uses the thing. `staysfixed record <a-name>` now opens
22
+ your product, follows what you do, and writes it down.
23
+
24
+ - **A click that moves the page is written down as the CLICK, not the destination.** Writing
25
+ the destination would open that page whether or not the button works, so a broken button
26
+ would have come back clean. That is the whole feature failing quietly, and it is guarded.
27
+ - **A recording is walked twice before it is accepted.** Anything that differs between the two
28
+ walks is not a step, it is noise, and the recording is refused rather than kept. A recording
29
+ that never got through its own steps even once is refused too — both walks failing
30
+ identically would otherwise read as perfectly steady.
31
+ - A project that asks for recorded sessions and has none is reported as not checked, never as
32
+ a pass.
33
+
34
+ ### Phones compare two builds side by side
35
+
36
+ Both phone surfaces refused a paired run because nobody knew whether an emulator or simulator
37
+ comes back the same twice. Nobody had ever measured it. Measured now, one build walked ten
38
+ times with a restore between every walk:
39
+
40
+ - **iOS: 725 of 725 addresses agreed across five pairs.**
41
+ - **Android: 301 of 309.** The eight that moved were the app's own freshly-made identity code
42
+ — ordinary wobble, which this tool already subtracts. A control run with the restore switched
43
+ off produced the same eight and no others, so the restore contributed exactly zero.
44
+
45
+ So paired is on for both, and every sentence that repeated the old guess now carries the
46
+ number instead. The real obstacle turned out to be somewhere else entirely: a paired phone run
47
+ needs a kept copy of the OLD build's APK or `.app`, because a build output is in no checkout of
48
+ the old commit. That is the new `reference` setting — and pointing it at the SAME file as the
49
+ new build is caught and reported as a hole on every journey, rather than comparing one build
50
+ with itself and calling it unchanged.
51
+
52
+ Also fixed on the way: `simctl` lists the newest iPhone first and the adapter took the last
53
+ one, asking for an iPhone 6s on iOS 27 — which Apple refuses with an empty message. The whole
54
+ iPhone surface was dark on a Mac that had everything it needed.
55
+
56
+ ### Three new kinds of product
57
+
58
+ - **Native Mac apps** (Swift or Objective-C, not Electron). Read through the Accessibility API
59
+ by JavaScript handed to macOS's own `osascript` — nothing is installed. Two builds of an
60
+ AppKit app differing by one line came back as exactly that one difference out of 75 things
61
+ read. **Two copies of one Mac app make one of them stop answering, and a silent app looks
62
+ exactly like an app with no controls** — so every read is cross-checked against the window
63
+ server's own list of what is on screen, and runs are one at a time.
64
+ - **Native Linux desktop apps** (GTK or Qt). Read over ssh through the accessibility bus every
65
+ screen reader already uses, with nothing installed on that machine: 190 controls in 305ms,
66
+ the same cost as the Windows surface. A machine with nobody logged in has no accessibility
67
+ bus at all and is told so, rather than reported as an app with no controls.
68
+ - **Browser extensions** (Chromium). The manifest is read as a contract — a permission that
69
+ appears, or a host permission widening from one named site to every site, is a change
70
+ somebody has to agree to, and that half needs no browser. Then the popup and options pages
71
+ are walked, and what a content script does to somebody else's page is measured by opening
72
+ that page with the extension and without it. What the background worker logs and asks the
73
+ network for is a named hole on every run, because both happen before anything can listen.
74
+
75
+ ### False all-clears found while building the above
76
+
77
+ - **The browser launcher passes `--disable-extensions` unconditionally**, and Chrome accepts
78
+ that alongside `--load-extension` without a word and loads nothing. Anything routed through
79
+ it would have walked a browser with no extension in it and reported everything as unchanged.
80
+ - **The settings lookup for a built program searched the whole file, not its own block.** `app`
81
+ means an iPhone bundle under `ios:` and a Mac bundle under `macos:`; `remoteExe` means the
82
+ same thing under `windows:` and `linux:`. Every one of them is scoped now, before the new
83
+ surfaces could make it true.
84
+ - **A `reference` pointing at the same file as the new build** compares a build with itself,
85
+ finds nothing, and says nothing changed. Caught by real path, on every journey.
86
+
11
87
  ## [0.11.1] — 2026-08-31
12
88
 
13
89
  Two defects that a Mac could never have shown, both caught by CI on Linux minutes after
package/README.md CHANGED
@@ -163,13 +163,17 @@ pretend otherwise.
163
163
  | The coverage ledger — every door counted, the unopened ones named, and the sentence saying so on every reply | **Works.** See [what it did not check](#what-it-did-not-check). |
164
164
  | Aiming a check at one kind of product, and refusing by name rather than checking something else | **Works.** |
165
165
  | Steps taken from your own test suite | **Works.** `--journeys suite` runs each test file twice inside the scratch copy, reports every check by name and why each failure failed, and stops after 90 seconds naming every file it did not reach. It is opt-in: running a stranger's whole suite twice on every check is not a thing to do by default. It catches what nothing else can — remove the penny-rounding from a `total()` and the product's own output does not move by one character, the discovered journeys say "nothing has changed", and the harvest names the check that turned red. |
166
- | Steps taken from a recorded session, or rejected at birth for not repeating twice | **Written, not wired.** The code is in `src/v2/journeys/` with tests around it, and nothing on the check path calls it yet. Ask for `--journeys recorded` and you are told so by name. |
167
- | Android APKs on an emulator | **The adapter is here.** It reads everything the APK declares with nothing installed and no Java, and where there is an emulator it installs one build at a time and walks it. Whether *this* machine can run one is a separate question, and `doctor` asks the adapter itself rather than keeping a second opinion — most of what it wants installs with a command; accepting Google's licence, once, needs a person. Two emulator snapshots restoring byte-identically is unproven, so Android compares against the stored record and says which mode it used. |
168
- | The iOS simulator | **The adapter is here.** It reads what the app bundle declares with nothing running, and where Xcode and a simulator runtime are present it installs one build at a time, boots it and reads what is on the screen. It is new. Paired running costs two `xcodebuild` passes, so it is for before a release rather than for every edit, and like Android it compares against the stored record and says which mode it used. Ask `doctor` what it is actually covering on your machine before trusting a clean run. |
166
+ | Steps taken from a recorded session, or rejected at birth for not repeating twice | **Works.** `staysfixed record <name>` opens your product, follows what you do in it, and writes the session down; `--journeys recorded` walks it on every later check. It is the one source that knows which four screens a person actually opens every morning, which no amount of reading the source can work out. Nothing is kept until it has been walked twice against the same build: anything that differs between those two walks is noise, not a step, and the recording is refused with the reason. Passwords and tokens are taken out on the way to the file. Proved end to end on a two-page site — break the second page and the journeys read out of the code say "nothing that worked has changed", because nothing in the source names that page; the recorded session names the sentence that changed. |
167
+ | Android APKs on an emulator | **The adapter is here.** It reads everything the APK declares with nothing installed and no Java, and where there is an emulator it installs one build at a time and walks it. Whether *this* machine can run one is a separate question, and `doctor` asks the adapter itself rather than keeping a second opinion — most of what it wants installs with a command; accepting Google's licence, once, needs a person. Two restores of one build were measured on 2026-08-31: 301 of 309 addresses agreed across five pairs, and the eight that moved were the app's own identity code, which the wobble subtraction already removes. So a paired run is offered — give it a kept copy of the old build's APK with `reference` under `android`, because a checkout of the old commit holds no build output. |
168
+ | The iOS simulator | **The adapter is here.** It reads what the app bundle declares with nothing running, and where Xcode and a simulator runtime are present it installs one build at a time, boots it and reads what is on the screen. It is new. Paired running costs two `xcodebuild` passes, so it is for before a release rather than for every edit, and two restores of one build were measured on 2026-08-31 on an iOS 27.0 simulator and 725 of 725 addresses agreed across five pairs, so a paired run is offered — it needs a kept copy of the old build's `.app` (`reference` under `ios`), because a checkout of the old commit holds no build output. Ask `doctor` what it is actually covering on your machine before trusting a clean run. |
169
169
  | Native Windows GUI (a real Win32 app, not an Electron one) | **Works,** and it has now been driven end to end: a real Win32 window on a Windows 11 desktop reached over ssh, 10 addresses read out of the UI Automation tree, a reference cut, and the next run compared against it. Nothing was installed on that machine — the program that reads the screen is sent down the connection each run. Windows shows one desktop, so two builds can never run at once: the comparison is genuinely weaker here than anywhere else, and a run says so rather than hiding it. |
170
+ | Browser extensions (Chromium) | **Works.** The manifest is read as a contract — a permission that appears, or a host permission that widens from one named site to every site, is reported as a change somebody has to agree to, and that half needs no browser at all. Then it loads the extension into a throwaway browser, walks its popup and options pages for what the screen means, and measures what its content scripts do to somebody else's page by opening that page with the extension and without it and comparing the two. Nothing reaches the internet. It does not read what the background worker logs or what it asks the network for — both happen in the instant the browser starts it, before anything can listen — and it says so on every run. |
171
+ | Native Mac GUI (a Swift or Objective-C app, not an Electron one) | **Works,** and it has been driven end to end on a real Mac: two builds of an AppKit app differing by one line, 75 addresses read out of the Accessibility API each, and exactly the one changed checkbox reported. Nothing is installed — the program that reads the screen is JavaScript handed to macOS's own `osascript`. One person has to allow it once under System Settings, Privacy & Security, Accessibility; macOS will not let any program grant itself that. One build at a time: two copies of one Mac app make one of them stop answering, which looks exactly like an app with no controls, so every read is cross-checked against the window server's own list. |
172
+ | Native Linux desktop GUI (GTK or Qt, not an Electron one) | **Works,** and it has been driven end to end: a real GTK window on an Ubuntu 24.04 desktop reached over ssh, every control read out of the accessibility bus every screen reader already uses, two runs of one build with zero unstable addresses, then a build with one checkbox ticked and one button disabled reported as exactly two differences and nothing else. Nothing was installed on that machine. A desktop shows one screen, so two builds can never run at once. A machine with nobody logged in has no accessibility bus at all, and is told so rather than reported as an app with no controls. |
170
173
 
171
- All eight surfaces — command-line tools, libraries, servers, websites, Electron,
172
- Android, iOS and native Windows have now actually been run against a real
174
+ All eleven surfaces — command-line tools, libraries, servers, websites, browser
175
+ extensions, Electron, Android, iOS, native Windows, native Linux and native Mac have now
176
+ actually been run against a real
173
177
  product, rather than only having an adapter and tests. That sentence was not true
174
178
  before 2026-08-31, and the four that were unproven each turned out to have at
175
179
  least one defect that only running them could find.
@@ -283,15 +287,59 @@ under instrumentation → recorded real sessions → the agent exploring one nam
283
287
  gap and freezing it into a replayable file → never a person clicking through an
284
288
  app.
285
289
 
286
- What is wired into `staysfixed check` today is the first of those, a journeys file
287
- you point it at, and when you ask for it — the project's own test suite. Each
288
- adapter reads your source and offers the journeys it can walk: routes, commands,
289
- screens, message channels. `--journeys <file>` names steps by hand. `--journeys
290
- suite` runs each test file twice inside the scratch copy, reports every check by
291
- name, and stops after 90 seconds naming every file it did not reach. Recorded
292
- sessions and the flake register are written and tested in `src/v2/journeys/`, and
293
- **nothing on the check path calls them yet**. Saying so is the point: a feature
294
- that exists in the repository and not in the run is not a feature you have.
290
+ Each adapter reads your source and offers the journeys it can walk: routes,
291
+ commands, screens, message channels. `--journeys <file>` names steps by hand.
292
+ `--journeys suite` runs each test file twice inside the scratch copy, reports every
293
+ check by name, and stops after 90 seconds naming every file it did not reach.
294
+ `--journeys recorded` walks the sessions you made with `staysfixed record` the
295
+ one source that knows how a person actually uses the product, because every other
296
+ one worked it out by reading, and reading only ever finds which doors exist, never
297
+ which four somebody opens every morning. A recording is walked twice against the
298
+ same build before it is kept, and refused with the reason if the two walks
299
+ disagree. The flake register is written and tested and **nothing on the check path
300
+ calls it yet**. Saying so is the point: a feature that exists in the repository and
301
+ not in the run is not a feature you have.
302
+
303
+ ### Recording the session you actually perform
304
+
305
+ ```
306
+ staysfixed record the-morning-round
307
+ staysfixed check --journeys recorded
308
+ ```
309
+
310
+ The first command opens your product, follows what you do in it, and writes the
311
+ session into `.staysfixed/journeys/`. Close the window when you are done. Commit
312
+ the file: it is the promise, not the evidence.
313
+
314
+ It is the answer to the one thing reading your source can never do. The code says
315
+ which doors exist; it never says which four you open every morning, in which
316
+ order, with what in the boxes. Tell it once and it is checked for ever. On a
317
+ two-page site whose second page is named nowhere in the source, breaking that page
318
+ reports **nothing** through the journeys read out of the code — the run says
319
+ "nothing that worked has changed" — and the recorded session names the sentence
320
+ that changed.
321
+
322
+ Nothing is kept until it repeats. The session is walked twice against the same
323
+ build, by the same walker a later check uses, before a byte of it reaches the
324
+ disk; if the two walks disagree about what exists, or it could not get through its
325
+ own steps, it is refused and told why. A journey that argues with itself goes red
326
+ on somebody else's laptop for no reason, and a check nobody trusts is worse than
327
+ no check.
328
+
329
+ Two details worth knowing. Passwords, tokens, card numbers and one-time codes are
330
+ taken out on the way to the file, and the count of what was hidden is written into
331
+ it. And a page move that a click caused is **not** written down as a step — a
332
+ journey that clicked a link and then opened that address would open the second
333
+ page whether or not the link still worked, which is exactly the false all-clear
334
+ this tool exists to never give.
335
+
336
+ Two limits, said plainly. `staysfixed record` follows you around a **web** app, in
337
+ a browser of the tool's own; nothing yet follows your hands around a desktop or a
338
+ phone, though `--journeys recorded` walks a recording of whatever surface its file
339
+ names. And a recorded journey is the weakest of the sources — one path somebody
340
+ happened to take, which goes stale when the interface moves — so anything reachable
341
+ from your code or your test suite should come from there instead. Before a run
342
+ walks one, it says by name what in it may not replay.
295
343
 
296
344
  ## Keeping it quiet
297
345
 
@@ -934,11 +982,14 @@ Honestly, so you know before you invest an afternoon.
934
982
  here.** No paired run is possible on a device in your hand: two builds cannot
935
983
  exist on one handset at once. Real iPhones and real Android handsets fall back
936
984
  to comparing against the stored record, and say so out loud on every run. The
937
- emulator and the simulator are the honest answer, and both of them compare
938
- against the stored record too for Android because two emulator snapshots
939
- restoring byte-identically is unproven, and for iOS because a paired run costs
940
- two `xcodebuild` passes and belongs before a release rather than after every
941
- edit. If you ship a phone app, ask `doctor` what it is actually covering before
985
+ emulator and the simulator are the honest answer, and both of them can be
986
+ paired. That was measured on 2026-08-31: iOS agreed at 725 of 725 addresses
987
+ across five pairs, Android at 301 of 309, and the eight Android ones that moved
988
+ were the app's own identity code noise the wobble subtraction already removes.
989
+ What a paired phone run needs from you is a kept copy of the old build's APK or
990
+ `.app`, named with `reference` under `android` or `ios`, because a build output
991
+ is not in any checkout of the old commit. Without it the run falls back to the
992
+ stored record and says so on every journey. If you ship a phone app, ask `doctor` what it is actually covering before
942
993
  you trust a clean result.
943
994
  - **It is not every possible state.** "Deep" means every door the code exposes and
944
995
  every journey it was given. Nothing can enumerate every state, and any tool
@@ -955,6 +1006,13 @@ Honestly, so you know before you invest an afternoon.
955
1006
  It earns the ask: take the penny-rounding out of a `total()` and the product's
956
1007
  own output does not move by one character — the discovered journeys say nothing
957
1008
  has changed, and the harvest names the check that turned red.
1009
+ - **A recorded session is the weakest of the sources, and it says so.** It is one
1010
+ path somebody happened to take, and it goes stale the moment the interface it
1011
+ describes moves — so anything reachable from the code or from your test suite
1012
+ comes from there instead. Before a run walks one it says what in it may not
1013
+ replay, by name. What it buys is the one thing the other sources cannot reach:
1014
+ the way a person actually uses the product, which no amount of reading can work
1015
+ out.
958
1016
  - **No hosted service, no dashboard, no accounts, no teams, nothing paid.** It is
959
1017
  a command and a folder of files in your repository.
960
1018
  - **Pictures still do not travel between operating systems.** Text is drawn
package/docs/design-v2.md CHANGED
@@ -17,12 +17,13 @@
17
17
  > servers, source reading, the web, Electron, Android, iOS and native Windows over ssh. The
18
18
  > MCP surface is seven tools and it is what `staysfixed mcp` serves.
19
19
  >
20
- > **What is written and not wired:** replaying a recorded session. The code is in
21
- > `src/v2/journeys/` with tests around it and nothing on the check path calls it; asking for
22
- > it is refused by name. Harvesting a project's own test suite was in this list and is now
23
- > wired `--journeys suite`, opt-in, held to a 90-second budget, every file it did not reach
24
- > named. Journeys otherwise come from what each adapter reads out of your source, plus a
25
- > journeys file you point it at.
20
+ > **What is written and not wired:** nothing. Replaying a recorded session was the last one
21
+ > and it landed on 2026-08-31: `staysfixed record` opens the product, follows what a person
22
+ > does, walks the session twice against the same build before keeping it, and `--journeys
23
+ > recorded` walks it on every later check. Harvesting a project's own test suite was in this
24
+ > list too and is wired `--journeys suite`, opt-in, held to a 90-second budget, every file
25
+ > it did not reach named. Journeys otherwise come from what each adapter reads out of your
26
+ > source, plus a journeys file you point it at.
26
27
  >
27
28
  > **What is permanent and will not change:** nothing irreversible is ever run — it is watched
28
29
  > at the call and refused at the effect, and the refusal is reported as missing coverage;
@@ -174,7 +175,7 @@ The FlaUI probe over SSH to his office box. Built only if he ever ships a non-El
174
175
 
175
176
  ## Honest limits
176
177
 
177
- WHERE "EVERY PLATFORM, NO COMPROMISE" DOES NOT HOLD. Web, Electron, CLI, libraries and servers get the full paired treatment. Android and the iOS SIMULATOR get it with real work and one unproven assumption each. Real iPhones and real Android handsets do not — no paired run is possible on a device he is holding. Windows native GUI cannot run two builds at once even in principle, because UI Automation reads whatever desktop is in front. And any product whose old build can no longer be compiled — a yanked dependency, a dead toolchain — falls back to comparing against the stored record from the last time it ran. That fallback is genuinely weaker: it reintroduces every cross-day difference the paired design exists to eliminate. It must announce itself in those words on every run, not degrade quietly.
178
+ WHERE "EVERY PLATFORM, NO COMPROMISE" DOES NOT HOLD. Web, Electron, CLI, libraries and servers get the full paired treatment. Android and the iOS SIMULATOR get it too the assumption each was resting on was measured on 2026-08-31 and held: iOS agreed at 725 of 725 addresses across five pairs of restores, Android at 301 of 309, and the eight Android ones that moved were the app's own freshly-made identity code, which the wobble subtraction already removes. What a paired phone run needs instead is a kept copy of the OLD build's APK or .app, because a build output is not in any checkout of the old commit. Real iPhones and real Android handsets do not — no paired run is possible on a device he is holding. Windows native GUI cannot run two builds at once even in principle, because UI Automation reads whatever desktop is in front. And any product whose old build can no longer be compiled — a yanked dependency, a dead toolchain — falls back to comparing against the stored record from the last time it ran. That fallback is genuinely weaker: it reintroduces every cross-day difference the paired design exists to eliminate. It must announce itself in those words on every run, not degrade quietly.
178
179
 
179
180
  WHAT "DEEP" HONESTLY MEANS. Every door the code exposes, and every journey the existing test suite already walks. It is NOT every possible state — nothing can enumerate that, and any tool claiming otherwise is lying. The defensible claim is: it catches breaks reachable from the journeys it has, and the coverage ledger names the doors it has never opened, so the hole is visible instead of pretended away.
180
181
 
@@ -88,9 +88,11 @@ What comes back:
88
88
  finds there — routes, commands, screens, message channels — and `--journeys <file>` names
89
89
  steps by hand. `--journeys suite` adds their own test suite: each test file runs twice inside
90
90
  the scratch copy, every check is reported by name, and it stops after 90 seconds naming every
91
- file it did not reach. Replaying a recorded session is written in `src/v2/journeys/` and is
92
- **not wired into a run**: ask for `--journeys recorded` and you are told so by name. Nothing
93
- quietly substitutes different steps and hands you a clean answer about them.
91
+ file it did not reach. `--journeys recorded` walks the sessions they recorded with
92
+ `staysfixed record` the one source that knows which screens a person actually opens, which
93
+ reading their source cannot work out. Ask for it in a project with no recordings and the run
94
+ stops and says so; nothing quietly substitutes different steps and hands you a clean answer
95
+ about them.
94
96
 
95
97
  ### 3. Take the first reading
96
98
 
@@ -126,16 +126,39 @@ Ranked, because this is the real workload question:
126
126
  4. **The agent exploring one named gap** and freezing it into a replayable file.
127
127
  5. Never a person clicking through an app.
128
128
 
129
- `--journeys <source>` picks between them and **one of those five is written and
130
- not wired.** A run walks (1) by default: each adapter reads your source and offers
131
- what it finds there. `--journeys <file>` names steps by hand, and `--journeys
132
- suite` adds (2), the project's own test suite each file run twice inside the
133
- scratch copy, every check reported by name, held to a 90-second budget with every
134
- file it did not reach named. Session replay lives in `src/v2/journeys/` with tests
135
- around it and nothing on the check path calls it; ask for `--journeys recorded` and
136
- you are told that by name rather than handed a clean result about steps something
137
- else chose. Saying so is the point: a feature that exists in the repository and not
138
- in the run is not a feature you have.
129
+ `--journeys <source>` picks between them. A run walks (1) by default: each adapter
130
+ reads your source and offers what it finds there. `--journeys <file>` names steps
131
+ by hand. `--journeys suite` adds (2), the project's own test suite — each file run
132
+ twice inside the scratch copy, every check reported by name, held to a 90-second
133
+ budget with every file it did not reach named. `--journeys recorded` adds (3), the
134
+ sessions somebody made with `staysfixed record` and kept in `.staysfixed/journeys`.
135
+
136
+ **Why (3) is worth having at all.** Everything above it was worked out by READING:
137
+ routes out of the source, exported names out of a package, screens out of a router.
138
+ Reading finds what the code says it does, and it is completely blind to the way a
139
+ person actually uses the thing — the four screens they open every morning, in that
140
+ order, with that data. A recorded session is the only channel that can learn that,
141
+ and once learned it is checked for ever. On a two-page site whose second page is
142
+ named nowhere in the source, breaking that page reports **nothing** through the
143
+ journeys read out of the code and is caught immediately by the recorded one.
144
+
145
+ **Nothing is recorded that has not repeated.** `staysfixed record` walks a fresh
146
+ session twice against the same build before it writes a single byte, using the very
147
+ same walker a later check uses, and refuses it if the two walks disagree about what
148
+ exists or if it could not get through the steps at all. A journey that argues with
149
+ itself would go red on somebody else's laptop for no reason, and version 1 already
150
+ proved where that ends: a flaky check does not get fixed, it gets ignored.
151
+
152
+ One trap is worth knowing about, because it is the shape of a false all-clear. When
153
+ a person clicks a link, the browser moves. Writing both down — click the link, then
154
+ open that address — makes a journey that opens the second page whether or not the
155
+ link still works, so a broken button comes back clean. A page move that arrives
156
+ right after something the person did is therefore dropped: it is the result of the
157
+ act, and the act is the step.
158
+
159
+ Asking for recorded sessions in a project that has none stops the run and says so,
160
+ naming the folder and the command that makes one. It never falls back to the steps
161
+ read out of the source and calls that a pass.
139
162
 
140
163
  ---
141
164
 
package/docs/mcp.md CHANGED
@@ -178,10 +178,12 @@ out of your source. `"suite"` walks the project's own test suite as well — eac
178
178
  test file run twice inside the scratch copy, every check reported by name and why
179
179
  each failure failed, stopping after 90 seconds with every file it did not reach
180
180
  named. It is worth asking for when a change could break behaviour the product's own
181
- output would never show. You can also pass a path to a journeys file. `"recorded"`
182
- is written in `src/v2/journeys/` and not yet wired into a run: ask for it and you
183
- are told so by name, rather than given a clean result about steps something quietly
184
- chose instead.
181
+ output would never show. `"recorded"` walks the sessions a person made with
182
+ `staysfixed record` and kept in `.staysfixed/journeys` the one source that knows
183
+ which screens somebody actually opens, which reading the source cannot work out. Ask
184
+ for it in a project with no recordings and the reply comes back blocked, naming the
185
+ folder and the command that makes one, rather than giving a clean result about steps
186
+ something quietly chose instead. You can also pass a path to a journeys file.
185
187
 
186
188
  ---
187
189
 
package/docs/settings.md CHANGED
@@ -406,12 +406,15 @@ anything behind in them.
406
406
  ## `android` — an APK on an emulator
407
407
 
408
408
  Installed on an emulator of its own, walked, then removed. Compared against the
409
- stored record: whether two emulator snapshots come back byte for byte is unproven,
410
- and every run says which mode it used.
409
+ stored record, or paired against the old build if you keep a copy of it. A snapshot
410
+ restore was measured on 2026-08-31 and repeats: 301 of 309 addresses agreed across
411
+ five pairs, and the eight that moved were the app's own freshly-made identity code,
412
+ which the wobble subtraction already removes. Every run says which mode it used.
411
413
 
412
414
  | Option | What it does |
413
415
  | --- | --- |
414
416
  | `apk` | The built package. Left out, it looks in the usual build folders. |
417
+ | `reference` | A kept copy of the OLD build's package, for a paired run. An APK is a build output, so a checkout of the old commit has none. Also accepts `{ apk: '...' }`. Left out, a paired run falls back to the stored record and says so. |
415
418
  | `avd` | Which emulator to use. Left out, it takes the first one that is **not** a Play Store image. |
416
419
  | `serial` | A device already plugged in, or an emulator already running. |
417
420
  | `journeys[]` / `screens[]` | Walks through the app: `{ name, steps: [{ tap: 'Sign in' }] }`. Left out, it opens the app and reads the first screen. |
@@ -459,9 +462,15 @@ Installed on a simulator, opened, and read — the same roles, names and states
459
462
  person hears read out to them. One build at a time. macOS only: nothing on Linux
460
463
  or Windows can run an iOS simulator.
461
464
 
465
+ A paired run is offered: two restores of one build were measured on 2026-08-31 on
466
+ an iOS 27.0 simulator and 725 of 725 addresses agreed across five pairs. It needs a
467
+ kept copy of the old build's `.app`, because a build output is not in any checkout
468
+ of the old commit.
469
+
462
470
  | Option | What it does |
463
471
  | --- | --- |
464
472
  | `app` | The built app bundle for the simulator. Left out, it looks where builds land. |
473
+ | `reference` | A kept copy of the OLD build's app bundle, for a paired run. A `.app` is a build output, so a checkout of the old commit has none. Also accepts `{ app: '...' }`. Left out, a paired run falls back to the stored record and says so. |
465
474
  | `deviceType`, `runtime` | Which simulator and which system: `'iPhone 17'`, `'iOS 26.4'`. Left out it picks a sensible one and says which. |
466
475
  | `device` | An exact simulator that already exists, by name or id. |
467
476
  | `journeys[]` | Walks through the app: `{ name, steps: [{ tap: 'Sign in' }] }`. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "staysfixed",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Prove that what already worked still works after an agent changed the code. Picture checks, guards for fixed bugs, a pre-release walkthrough, and known-good markers \u2014 as a CLI and as an MCP server.",
5
5
  "type": "module",
6
6
  "license": "MIT",