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 +76 -0
- package/README.md +77 -19
- package/docs/design-v2.md +8 -7
- package/docs/getting-started.md +5 -3
- package/docs/how-v2-works.md +33 -10
- package/docs/mcp.md +6 -4
- package/docs/settings.md +11 -2
- package/package.json +1 -1
- package/src/v2/adapters/android.js +220 -11
- package/src/v2/adapters/extension.js +1988 -0
- package/src/v2/adapters/ios-driver.js +95 -12
- package/src/v2/adapters/ios.js +220 -10
- package/src/v2/adapters/linux-driver.js +1028 -0
- package/src/v2/adapters/linux.js +1324 -0
- package/src/v2/adapters/macos-driver.js +913 -0
- package/src/v2/adapters/macos.js +1374 -0
- package/src/v2/browsers.js +9 -1
- package/src/v2/check.js +133 -13
- package/src/v2/cli.js +2 -0
- package/src/v2/coverage.js +1 -1
- package/src/v2/detect.js +5 -2
- package/src/v2/doctor.js +122 -15
- package/src/v2/init.js +12 -2
- package/src/v2/journeys/index.js +3 -3
- package/src/v2/journeys/record-session.js +839 -0
- package/src/v2/journeys/record.js +12 -0
- package/src/v2/mcp/tools.js +8 -15
- package/src/v2/types.js +1 -1
- package/src/v2/watch/events.js +6 -0
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 | **
|
|
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
|
|
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
|
|
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
|
|
172
|
-
Android, iOS
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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:**
|
|
21
|
-
>
|
|
22
|
-
>
|
|
23
|
-
>
|
|
24
|
-
>
|
|
25
|
-
>
|
|
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
|
|
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
|
|
package/docs/getting-started.md
CHANGED
|
@@ -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.
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
package/docs/how-v2-works.md
CHANGED
|
@@ -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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
410
|
-
and
|
|
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.
|
|
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",
|