staysfixed 0.7.1 → 0.8.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/README.md +193 -55
  3. package/docs/design-v2.md +24 -4
  4. package/docs/getting-started.md +18 -5
  5. package/docs/guards.md +2 -2
  6. package/docs/how-v2-works.md +12 -11
  7. package/docs/mcp.md +17 -8
  8. package/docs/settings.md +549 -0
  9. package/docs/watching.md +10 -4
  10. package/examples/staysfixed.config.electron.js +17 -6
  11. package/examples/staysfixed.config.web.js +22 -5
  12. package/package.json +2 -1
  13. package/src/cli/index.js +55 -46
  14. package/src/cli/watch-flags.js +54 -0
  15. package/src/core/config.js +23 -3
  16. package/src/guard/run.js +49 -1
  17. package/src/report/console.js +15 -2
  18. package/src/v2/adapters/android-driver.js +6 -1
  19. package/src/v2/adapters/android.js +97 -2
  20. package/src/v2/adapters/contract.js +42 -5
  21. package/src/v2/adapters/electron.js +72 -6
  22. package/src/v2/adapters/http.js +11 -2
  23. package/src/v2/adapters/ios-driver.js +64 -14
  24. package/src/v2/adapters/ios.js +247 -25
  25. package/src/v2/adapters/process.js +728 -66
  26. package/src/v2/adapters/python.js +495 -0
  27. package/src/v2/adapters/source.js +373 -18
  28. package/src/v2/adapters/web-driver.js +94 -24
  29. package/src/v2/adapters/web.js +142 -9
  30. package/src/v2/adapters/windows.js +18 -1
  31. package/src/v2/browsers.js +9 -1
  32. package/src/v2/cause.js +61 -17
  33. package/src/v2/check.js +530 -66
  34. package/src/v2/ci.js +130 -35
  35. package/src/v2/cli.js +42 -24
  36. package/src/v2/cluster.js +164 -13
  37. package/src/v2/coverage.js +43 -176
  38. package/src/v2/detect.js +308 -60
  39. package/src/v2/doctor.js +345 -47
  40. package/src/v2/init.js +162 -61
  41. package/src/v2/intent.js +9 -23
  42. package/src/v2/journeys/from-suite.js +336 -30
  43. package/src/v2/journeys/index.js +99 -6
  44. package/src/v2/mcp/tools.js +10 -11
  45. package/src/v2/normalise.js +169 -23
  46. package/src/v2/observation.js +19 -33
  47. package/src/v2/rank.js +216 -23
  48. package/src/v2/reference.js +40 -10
  49. package/src/v2/remote.js +113 -18
  50. package/src/v2/run.js +103 -14
  51. package/src/v2/sealed.js +0 -20
  52. package/src/v2/selfcheck.js +190 -13
  53. package/src/v2/ship.js +29 -5
  54. package/src/v2/store.js +67 -1
  55. package/src/v2/types.js +12 -2
  56. package/src/v2/waiver.js +64 -54
  57. package/src/v2/watch/events.js +60 -215
  58. package/src/v2/watch/focus.js +14 -4
  59. package/src/v2/watch/panel.js +167 -17
package/README.md CHANGED
@@ -52,14 +52,22 @@ npx staysfixed check
52
52
  No account, no sign-up, no server anywhere, nothing uploaded. It works in any
53
53
  project in any language — it only needs to be able to run your product.
54
54
 
55
- Requirements: **Node 22 or newer**. Everything else depends on what you are
56
- watching, and the tool works out what it has:
55
+ Requirements: **Node 22 or newer**, and **git** your project has to be a git
56
+ repository. Not for history's sake: git is how an old build is put back on the
57
+ machine to be walked live, and how a difference is measured against the code you
58
+ just changed. A folder with no git in it is refused outright rather than checked
59
+ against a guess. Everything else depends on what you are watching, and the tool
60
+ works out what it has:
57
61
 
58
62
  ```
59
63
  npx staysfixed doctor
60
64
  npx staysfixed doctor --json # the same answer, for an agent
61
65
  ```
62
66
 
67
+ Everything each kind of product needs — the exact programs, the exact install
68
+ commands, the settings keys and the licences only a person can accept — is in
69
+ [docs/settings.md](docs/settings.md).
70
+
63
71
  `doctor` is the first thing you should run and the first thing an agent should
64
72
  call. It says what it can check on this machine, what it cannot, what is missing,
65
73
  and the exact command that would fix each gap — and it never suggests setting up
@@ -94,23 +102,27 @@ phone app here to check.
94
102
 
95
103
  ### What a fresh install downloads, and what it does not
96
104
 
97
- `npm install staysfixed` pulls **two small packages and nothing else** —
98
- `pixelmatch` and `pngjs`, under a megabyte together. No browser, no runtime,
99
- nothing that takes minutes.
105
+ `npm install staysfixed` pulls **three packages and no browser** — `pixelmatch`,
106
+ `pngjs` and `playwright-core`, about 13MB together on a Mac. `playwright-core` is
107
+ the part that drives a browser; it is deliberately the version that **downloads
108
+ none**, so nothing here takes minutes and nothing lands in a shared cache behind
109
+ your back.
100
110
 
101
- Checking a **website** needs a browser. Rather than make everybody who only
102
- wanted to check a command-line tool wait for one, that is a separate step you
103
- take when you need it:
111
+ Checking a **website** needs an actual browser, and the tool looks for one you
112
+ already have before asking for anything: Chrome for Testing, Chromium, Edge or
113
+ Chrome. If there is none on the machine at all, that is one command:
104
114
 
105
115
  ```
106
- npm install --save-dev playwright && npx playwright install chromium
116
+ npx playwright install chromium
107
117
  ```
108
118
 
109
- Measured on a Mac in August 2026, that is about **18MB of packages** in your
110
- project and about **570MB of browser** in a shared cache outside it 371MB for
111
- Chrome for Testing and 196MB for its headless shell downloaded once per
112
- machine, not once per project. `doctor` tells you when you need it, and it is one
113
- of the things an agent can simply do without asking you.
119
+ Measured on a Mac in August 2026 that is about **570MB** in a shared cache
120
+ outside your project 371MB for Chrome for Testing and 196MB for its headless
121
+ shell downloaded once per machine, not once per project. `doctor` tells you if
122
+ you need it, and it is one of the things an agent can simply do without asking
123
+ you. A project that already drives its own tests with the full `playwright`
124
+ package is used as it is and never asked to install a second copy of the same
125
+ driver.
114
126
 
115
127
  Checking a **desktop app** needs no browser at all and no download: the app is
116
128
  its own Chromium, and the tool drives it over its own debugging port.
@@ -150,21 +162,33 @@ pretend otherwise.
150
162
  | The reference cut when you ship, sealed intents, the waiver budget, and escalations in your closing summary | **Works.** This page describes what it actually does. |
151
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). |
152
164
  | Aiming a check at one kind of product, and refusing by name rather than checking something else | **Works.** |
153
- | Steps taken from your own test suite, 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. Journeys today come from what each adapter reads out of your source, plus any `--journeys` file you name. |
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. |
154
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. |
155
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. |
156
169
  | Native Windows GUI (a real Win32 app, not an Electron one) | **The probe is here**, driven over ssh to any machine that reaches a Windows desktop — a WSL shell on one counts, and nothing is installed on it. Windows shows one desktop, so two builds can never run at once: the comparison is genuinely weaker here than anywhere else. |
157
170
 
158
171
  `staysfixed check` is the front door for both. Version 1's flags still mean
159
- exactly what they meant yesterday — `--pictures`, `--guards`, `--watch` and
160
- `--only` reach the same code they always did, on the settings you already have,
161
- and `staysfixed mcp --v1` still serves the picture tools. Nobody who installed
162
- this last week has to change anything.
163
-
164
- The one thing that did change: `staysfixed init` now writes settings for the
165
- difference engine. On a product with no screen those settings have no `app` block
166
- in them, and the picture commands say so plainly rather than telling you to invent
167
- a web address. Add an `app` block yourself if you want picture checks too.
172
+ exactly what they meant yesterday — `--pictures`, `--guards` and `--only` reach
173
+ the same code they always did, on the settings you already have, and `staysfixed
174
+ mcp --v1` still serves the picture tools. Nobody who installed this last week has
175
+ to change anything.
176
+
177
+ **Two things did change, and they are worth reading if you installed this before
178
+ today.**
179
+
180
+ `--watch` moved. `staysfixed check --watch` opens **version 2's** panel the one
181
+ built for a difference engine, which draws journeys, wobble, findings and a
182
+ verdict. Version 1's panel drew approved pictures side by side, which this tool no
183
+ longer has. `--pictures --watch` and `--guards --watch` still open version 1's
184
+ panel over version 1's run, so the only person whose command changed meaning is
185
+ the one who typed `--watch` on its own and got a picture check they did not ask
186
+ for. Everything about the panel is in [docs/watching.md](docs/watching.md).
187
+
188
+ `staysfixed init` now writes settings for the difference engine. On a product with
189
+ no screen those settings have no `app` block in them, and the picture commands say
190
+ so plainly rather than telling you to invent a web address. Add an `app` block
191
+ yourself if you want picture checks too.
168
192
 
169
193
  ## How it proves nothing changed
170
194
 
@@ -181,6 +205,18 @@ There is no tolerance setting in version 2 and there is not going to be one.
181
205
  Tolerance knobs are how tools like this die — too loose to catch the real thing,
182
206
  too tight to leave switched on.
183
207
 
208
+ Version 1's picture check is the proof, and it was caught doing exactly that.
209
+ Its default allowed 0.05% of a picture through, with a comment saying that was
210
+ "enough for font hinting noise, nowhere near enough to hide a missing
211
+ stylesheet". On a 2880×1800 screenshot, 0.05% is **2,592 pixels**. Taking one
212
+ letter out of a page's main heading moves **593** — so the check reported
213
+ "Everything that worked still works" over a page anybody could see was wrong.
214
+ Ten fresh takes of that same build differ by **zero** pixels, because the freeze
215
+ layer underneath is thorough enough to make that true. So version 1's default is
216
+ now zero as well: a project whose product genuinely wobbles can set
217
+ `tolerance.pixels`, and every run then says in words how many pixels that
218
+ setting just swallowed.
219
+
184
220
  It also catches a bug class no screenshot tool has ever caught. A path that was
185
221
  **steady** in the old build and **wobbles** in the new one means the change made
186
222
  something unpredictable. That is a finding, even though no single value can be
@@ -241,13 +277,15 @@ under instrumentation → recorded real sessions → the agent exploring one nam
241
277
  gap and freezing it into a replayable file → never a person clicking through an
242
278
  app.
243
279
 
244
- What is actually wired into `staysfixed check` today is the first of those and a
245
- journeys file you point it at: each adapter reads your source and offers the
246
- journeys it can walk routes, commands, screens, message channels — and
247
- `--journeys <file>` names steps by hand. The suite harvest, recorded sessions and
248
- the flake register are written and tested in `src/v2/journeys/`, and **nothing on
249
- the check path calls them yet**. Saying so is the point: a feature that exists in
250
- the repository and not in the run is not a feature you have.
280
+ What is wired into `staysfixed check` today is the first of those, a journeys file
281
+ you point it at, and when you ask for it the project's own test suite. Each
282
+ adapter reads your source and offers the journeys it can walk: routes, commands,
283
+ screens, message channels. `--journeys <file>` names steps by hand. `--journeys
284
+ suite` runs each test file twice inside the scratch copy, reports every check by
285
+ name, and stops after 90 seconds naming every file it did not reach. Recorded
286
+ sessions and the flake register are written and tested in `src/v2/journeys/`, and
287
+ **nothing on the check path calls them yet**. Saying so is the point: a feature
288
+ that exists in the repository and not in the run is not a feature you have.
251
289
 
252
290
  ## Keeping it quiet
253
291
 
@@ -338,6 +376,18 @@ verdict reading *nothing that worked has changed*. It is arithmetically true and
338
376
  it would let a real regression through. That run comes back as **`NOTHING WAS
339
377
  ACTUALLY COMPARED`**, it is not a pass, and it exits non-zero.
340
378
 
379
+ **The cold start is the same thing, and it is now marked the same way.** The very
380
+ first run on a project — before anybody has shipped once with the hook in place —
381
+ has no reference at all, rather than a reference with nothing in it. For a while
382
+ only the command line caught that: it said *"Nothing to compare against yet… this
383
+ run proves nothing about what still works"* and exited 2, while `--json` and
384
+ `staysfixed_check` over MCP both carried `ok: true` with none of those words. An
385
+ agent reads the fields, not the sentence, so the two interfaces that matter most
386
+ reported a pass over a run that compared nothing — the exact failure this tool
387
+ exists to prevent, produced by the tool itself. Both now answer `ok: false` with
388
+ **`NOTHING WAS ACTUALLY COMPARED`**. `reference.id` is still worth reading — an
389
+ empty string means nothing was compared, whatever else a reply says.
390
+
341
391
  ### The silences that were found and closed
342
392
 
343
393
  Every one of these left something invisible while the answer looked complete —
@@ -364,6 +414,35 @@ to ask the question a different way.
364
414
  | Ask a machine `command -v powershell.exe` over ssh to find out whether Windows sits behind it — a question that answers "no" on a machine with Windows right there, because that path is added by an interactive login shell and ssh does not run one. It also read a **refusal** as an answer, so `github.com` was listed as a machine to run checks on, and as a Windows desktop | Asks the filesystem for the three places PowerShell actually lives, using the one list the code that later drives it uses. Reads standard output only, and matches the whole line, so a host that quotes your command back cannot answer for itself |
365
415
  | Name only the first eight machines in an ssh config and drop the rest without a word | Dials sixteen, and anything past that is named as not dialled rather than left out |
366
416
  | Report Docker as present because the command is on the path, on a Mac where Docker Desktop is shut and nothing it promises would work | Asks the engine for its version, and says "installed but not answering" when that is the truth |
417
+ | Read the **commented-out examples** in a settings file as settings. `staysfixed init` comments out every option that does not apply to your project, so nothing is hidden from you — and doctor searched the raw text. On a folder holding one script it announced "Electron desktop apps: **Covered.** It opens release/mac-arm64/Your App.app", and an Android app beside it. A surface called covered when nothing will ever be walked on it is the worst answer this tool can give | Comments are taken away before anything is read out of the file, with strings respected so an address keeps its two slashes. The file is still never loaded — doctor must not run your code to answer a question about your machine |
418
+ | Ask for a build step on a plain command-line tool. A script that runs from source is recorded as "not built", because there is nothing to build, and that was read as "it has not been built yet" — so a fresh install told its owner to name the command that builds a file sitting right there, in the same breath as offering to run it | Nothing is asked for when there is nothing to build, or when a command to run it has already been worked out |
419
+
420
+ ### The silences that are still open
421
+
422
+ The table above is the honest half of a habit, and it would be a dishonest half
423
+ on its own: it lists what was found and closed, which reads as though nothing is
424
+ left. These are the ones that are **still open today**, found the same way, by
425
+ reading the code rather than the documentation. Every one of them can end in a
426
+ clean-looking answer, and the file that would close each one is named so nobody
427
+ has to take this on trust.
428
+
429
+ Seven rows left this table in one night. They are in the closed table above now:
430
+ a wobble storm that needed twelve addresses before it counted, so ten of eleven
431
+ wobbling passed; a desktop control compared to its first 200 characters; an
432
+ unreadable folder losing every page of a website behind it; two screens with one
433
+ name collapsing into one walk; a waiver pinned to the first 40 differences of a
434
+ finding rather than all of them; and three iOS ceilings that stopped counting
435
+ without saying so. What is below is what is genuinely left.
436
+
437
+ | What is invisible | Where |
438
+ | --- | --- |
439
+ | **What the scratch copy leaves out is left out of *both* builds.** A product that actually needs one of the folders named under `process.skip` fails the same way twice, so the difference engine sees no difference and the run reads clean. The defaults are only caches and reports, and the bar is written down — but a project that adds to that list can hide a real break from itself. | `src/v2/adapters/process.js` — `SKIP_BY_DEFAULT` |
440
+ | **Working out what a repository makes reads at most 300 files, four folders deep, and skips any file over 2MB.** A product it never notices is never configured and never walked, so it is missing from the coverage ledger rather than named in it — the one kind of gap the ledger cannot show you. | `src/v2/detect.js` |
441
+ | **Values nested more than 64 levels deep are compared as "too deep" rather than by their contents.** It will never go quiet about one, but it cannot say what inside it moved. This only ever applies to a value read back off the disk; observations made in this run refuse that depth at the door. | `src/v2/observation.js` |
442
+ | **A change buried in the middle of an output larger than 64KB can be missed.** Both ends are kept and compared along with the exact number of bytes dropped, so a middle that grew or shrank shows up. One that changed without changing length does not. | `src/v2/adapters/contract.js` — `trimForStorage` |
443
+ | **How long anything took is recorded and never compared.** A build that got twice as slow is not a finding. This one is deliberate and permanent — a stopwatch on a shared machine measures the machine at least as much as the product — and a build that *hangs* is still caught, because it is stopped for taking too long and how it finished is compared exactly. | by design |
444
+ | **On Windows and as root, one self-check case cannot run.** It takes away the permission to write, to prove the tool still answers when the disk gives out; neither honours that. The case reports itself as untested rather than as a pass, and the closing line counts it separately. | `src/v2/selfcheck.js` |
445
+ | **The distance from a finding to your edit has three ceilings: 4,000 source files, 8 imports out, and 400KB per file.** Past any of them the ranking for that finding is a guess. It is not silent — a run that hit a ceiling says so and names the files it could not read — but the order is worth less than the rest of the list. | `src/v2/rank.js` |
367
446
 
368
447
  ---
369
448
 
@@ -422,9 +501,12 @@ An agent's only door is a waiver, and it passes four machine-checked gates:
422
501
  3. **Five waivers between one ship and the next.** Past five it is not a change
423
502
  with side effects, it is a rewrite, and a person looks at a rewrite. Sealing
424
503
  another intent does not buy five more.
425
- 4. **Every waiver is fingerprinted to one exact difference** and dies the moment
426
- the reference moves. Change the value it was written about and it stops
427
- covering anything.
504
+ 4. **Every waiver is fingerprinted to the difference it was written about** and
505
+ dies the moment the reference moves. Change that value and it stops covering
506
+ anything. The pin is the finding's title, up to twenty of its addresses and up
507
+ to forty of its differences — see [the silences that are still
508
+ open](#the-silences-that-are-still-open) for what that means on a very large
509
+ finding.
428
510
 
429
511
  Every waiver is counted out loud in the reply. "Nothing changed", "nothing ran"
430
512
  and "everything was waived" read identically otherwise, and two of those three
@@ -654,18 +736,69 @@ that is correct**: the picture commands (`status`, `walk`, `approve`, `mark`,
654
736
  they need an `app` and say so plainly if there is not one. `staysfixed check`
655
737
  needs no `app` at all.
656
738
 
657
- A `staysfixed.config.json` works too, with a declarative `steps` form and no
658
- functions — so a Rust, Python or Go project can use the tool without anybody
659
- writing JavaScript.
739
+ ### `process.skip` what to leave out of the scratch copy
740
+
741
+ Every run works in a **throwaway copy of your project**, so a build can write
742
+ wherever it likes without touching the folder you are working in. That copy is
743
+ cloned rather than copied: on a Mac and on most Linux filesystems it is one call
744
+ per file that moves no bytes at all and shares the blocks until something writes
745
+ to them. Measured on a twelve-gigabyte project, most of it an iOS build folder:
746
+ **41.5 seconds and no disk used.** Where the filesystem cannot clone, it falls
747
+ back to a real copy — slower, never wrong.
748
+
749
+ If that copy still takes over twenty seconds the run says so and points you here.
750
+ You can name folders to leave behind:
660
751
 
661
- Three fully commented examples are in [`examples/`](examples/):
662
- [a web app](examples/staysfixed.config.web.js),
663
- [an Electron app](examples/staysfixed.config.electron.js), and
664
- [a guard](examples/guards/the-sidebar-still-collapses.js). All three are written
665
- for the picture check, which is the half that needs an `app`.
752
+ ```js
753
+ export default {
754
+ product: 'your-app',
755
+ process: {
756
+ // Folders not worth copying into the scratch build.
757
+ skip: ['DerivedData', '.gradle'],
758
+ },
759
+ };
760
+ ```
666
761
 
667
- The full reference lives with the code it configures, and the design behind all
668
- of it is in [docs/how-v2-works.md](docs/how-v2-works.md).
762
+ **The bar for putting something on that list is deliberately high: only things
763
+ that are regenerated on demand and read by nothing.** Caches, coverage reports,
764
+ scratch output. Anything you skip that turns out to matter makes a run pass for
765
+ the wrong reason — and a run that passes for the wrong reason is the single
766
+ failure this whole tool exists to prevent. Build output, `node_modules`,
767
+ lockfiles, fixtures and configuration are all copied, every time, because a check
768
+ that runs against a different set of files than the real product is not checking
769
+ the real product. If you are not sure, leave it in and let the run be slower.
770
+
771
+ `skip` can only ever **add** to what is already left behind — `.git`,
772
+ `.staysfixed`, `.turbo`, `.nyc_output`, `coverage`, `.pytest_cache`,
773
+ `__pycache__` and `.DS_Store`. There is no way to switch one of those back on,
774
+ because doing so would only ever make runs slower.
775
+
776
+ A `staysfixed.config.json` works too, with a declarative `steps` form and no
777
+ functions — so a Rust, Python or Go project can use the tool without anybody
778
+ writing JavaScript. It may also live at `.staysfixed/config.js` (or `.mjs`, or
779
+ `.json`) if you would rather not have another file in the root.
780
+
781
+ **Every option, and what each kind of product needs on the machine**, is in
782
+ [docs/settings.md](docs/settings.md) — every settings key per block, and for each
783
+ surface the exact programs, environment variables, install commands and
784
+ permissions, rather than "you may need Xcode".
785
+
786
+ **Do not copy an example to set the difference engine up.** Run `staysfixed
787
+ init`: it reads what is actually in your project and writes a settings file with
788
+ every option in it — the ones that do not apply commented out rather than left
789
+ out — filled in from your own code. That is better than any example, because it
790
+ is about your project rather than somebody else's.
791
+
792
+ The three fully commented examples in [`examples/`](examples/) — [a web
793
+ app](examples/staysfixed.config.web.js), [an Electron
794
+ app](examples/staysfixed.config.electron.js) and [a
795
+ guard](examples/guards/the-sidebar-still-collapses.js) — are all **version 1's
796
+ shape**, for the picture check, which is the half that needs an `app`. Each file
797
+ says so at the top now. Copy one into a project and a plain `staysfixed check`
798
+ will tell you the settings do not name anything to open, which is correct and
799
+ confusing if you were not expecting it.
800
+
801
+ The design behind all of it is in [docs/how-v2-works.md](docs/how-v2-works.md).
669
802
 
670
803
  ---
671
804
 
@@ -764,10 +897,11 @@ Honestly, so you know before you invest an afternoon.
764
897
  normalisation rule for whatever is moving, not by trusting the clean-looking run
765
898
  underneath it.
766
899
  - **What normalisation rubbed out is not itemised on every run.** The rules are in
767
- your repository, they are listed by `staysfixed rules`, and the capture is
768
- stamped with which set was useda run comparing against a record tidied by a
769
- different set says so. What it does not yet do is print, per run, every value a
770
- rule rewrote. Anything a rule covers is not being watched, and that is the point
900
+ your repository one file, `.staysfixed/rules.json`, which you read like any
901
+ other file in a pull requestand the capture is stamped with which set was
902
+ used, so a run comparing against a record tidied by a different set says so.
903
+ There is no command that lists them and no per-run report of what each one
904
+ rewrote. Anything a rule covers is not being watched, and that is the point
771
905
  of the rule; just know that adding a broad one is how you go blind on purpose.
772
906
  - **Ranking reads your source, and it gives up on very large trees.** Distance
773
907
  from the code you just changed is what sorts the list, and it reads up to 4,000
@@ -798,13 +932,17 @@ Honestly, so you know before you invest an afternoon.
798
932
  every journey it was given. Nothing can enumerate every state, and any tool
799
933
  claiming otherwise is lying. The coverage ledger names the doors it has never
800
934
  opened, so the hole is visible instead of pretended away.
801
- - **Your own test suite is not walked for you yet.** The code that harvests a
802
- project's existing tests as journeys is written and tested in
803
- `src/v2/journeys/`, and nothing on the check path calls it. Ask for it
804
- `--journeys suite`, or `journeys: "suite"` over MCP and you are told that, by
805
- name, rather than being given a clean result about steps it silently chose
806
- instead. Today the steps come from what each adapter reads out of your source
807
- and from a journeys file you point it at.
935
+ - **Your own test suite is walked only when you ask.** `--journeys suite`, or
936
+ `journeys: "suite"` over MCP, runs each test file twice inside the scratch copy
937
+ and reports every check by name, why each failure failed, and a fingerprint of
938
+ the test file itself so an edited test says plainly that the change is yours.
939
+ It is off by default because running a stranger's whole suite twice on every
940
+ check would make this too slow to leave switched on, and a check nobody can
941
+ afford to run is a check nobody runs. It is held to 90 seconds and **every
942
+ file it did not reach is named**, one by one, never "some tests were skipped".
943
+ It earns the ask: take the penny-rounding out of a `total()` and the product's
944
+ own output does not move by one character — the discovered journeys say nothing
945
+ has changed, and the harvest names the check that turned red.
808
946
  - **No hosted service, no dashboard, no accounts, no teams, nothing paid.** It is
809
947
  a command and a folder of files in your repository.
810
948
  - **Pictures still do not travel between operating systems.** Text is drawn
package/docs/design-v2.md CHANGED
@@ -17,10 +17,12 @@
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:** harvesting a project's own test suite as journeys, and
21
- > replaying a recorded session. The code is in `src/v2/journeys/` with tests around it and
22
- > nothing on the check path calls it. Asking for either is refused by name. Journeys today
23
- > come from what each adapter reads out of your source, plus a journeys file you point it at.
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.
24
26
  >
25
27
  > **What is permanent and will not change:** nothing irreversible is ever run — it is watched
26
28
  > at the call and refused at the effect, and the refusal is reported as missing coverage;
@@ -91,6 +93,13 @@ Where I rejected the alternative: the differential design would let the agent de
91
93
 
92
94
  ## Platforms, in build order
93
95
 
96
+ > **Do not install anything off this section.** These were the *plans* for driving each
97
+ > platform, and four of them were replaced during the build — Appium, UiAutomator2,
98
+ > WebDriverAgent, FlaUI and a Java runtime are named below and **none of them is used**. What
99
+ > each surface actually needs, with the exact command, is in
100
+ > [settings.md](settings.md); what this machine actually has is
101
+ > `staysfixed doctor`. See [what the plan got wrong](#what-the-plan-got-wrong-2026-08-30).
102
+
94
103
  ### 1. CLI tools and libraries
95
104
 
96
105
  - **Driven by:** Plain child-process I/O: stdout, stderr, exit code, files touched, processes spawned, plus the exported API surface read from the source.
@@ -267,6 +276,17 @@ the README's list of silences. The rule that came out of it is worth more than t
267
276
  and match the whole line, because a host that quotes your command back can otherwise answer
268
277
  for itself.**
269
278
 
279
+ **Four of the tools named for driving the phones and Windows were never used.** The plan
280
+ said Appium 3 with UiAutomator2 for Android, WebDriverAgent for the iOS meaning tree, a Java
281
+ runtime for both, and a .NET probe built on FlaUI for Windows. What shipped needs none of
282
+ them. Android reads the APK directly — **no Java at all** — and drives the emulator through
283
+ `adb`. iOS uses `xcrun simctl` and a small reader compiled at run time with `clang` from the
284
+ Xcode command line tools. Windows sends a PowerShell script down an ssh connection each run
285
+ and installs nothing on the far machine. Every one of those replacements removed a
286
+ dependency rather than adding one, which is why they happened; the cost is that this page,
287
+ left alone, would have an agent installing four things nobody needs. The list that is kept
288
+ current is [settings.md](settings.md).
289
+
270
290
  **The self-check corpus was the best decision in the whole design,** and the reason is not
271
291
  the one given here. It was justified as proving the engine catches things. What it actually
272
292
  did was catch the engine being *perturbable*: it came back "1 of 9 wrong" with the test suite
@@ -86,10 +86,11 @@ What comes back:
86
86
 
87
87
  **Where the steps come from, today.** Each adapter reads their source and offers what it
88
88
  finds there — routes, commands, screens, message channels — and `--journeys <file>` names
89
- steps by hand. Harvesting their own test suite and replaying a recorded session are written
90
- in `src/v2/journeys/` and are **not wired into a run**: ask for `--journeys suite` or
91
- `--journeys recorded` and you are told so by name. Nothing quietly substitutes different
92
- steps and hands you a clean answer about them.
89
+ steps by hand. `--journeys suite` adds their own test suite: each test file runs twice inside
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.
93
94
 
94
95
  ### 3. Take the first reading
95
96
 
@@ -135,6 +136,11 @@ Every check answers with one object. The fields that decide what you do:
135
136
  something that used to give the same answer every time stopped doing so.
136
137
  - **`blocked`** — the run could not happen. This is neither a pass nor a failure. Never
137
138
  report a blocked run as "nothing changed".
139
+ - **`reference.id`** — check this before you believe `ok`. An **empty string** means there was
140
+ nothing on record to compare this build against, so the run proved nothing at all. It comes
141
+ back with `ok: true`, because arithmetically nothing came back different — and it is not a
142
+ pass. It happens on every project until somebody has run `staysfixed ship` once. The command
143
+ line exits 2 on it; the JSON does not say so in a field yet, so read this one.
138
144
  - **`mode`** — `paired` means the old build was booted and walked here, in this minute.
139
145
  `stored-record` means it was compared against what the old build wrote down last time,
140
146
  which is genuinely weaker. When it is `stored-record`, `modeWarning` holds the sentence to
@@ -209,7 +215,9 @@ Four checks, in order, and they take about a minute:
209
215
  2. `staysfixed init --json` returns `ok: true`, and `plan.readiness` has at least one product
210
216
  whose state is `ready`.
211
217
  3. `staysfixed check --paired` finishes with `blocked` absent and `coverage.paths` above zero.
212
- Zero addresses observed means it walked nothing, whatever else it says.
218
+ Zero addresses observed means it walked nothing, whatever else it says. On a project that
219
+ has never shipped, `reference.id` will be empty and that run proves nothing — expected, and
220
+ over as soon as they ship once.
213
221
  4. Break something on purpose — change a line of printed text, delete a route — run
214
222
  `staysfixed check`, and confirm it names it. Then put it back.
215
223
 
@@ -290,6 +298,11 @@ Say these once, when someone asks how much it covers. They are permanent, they a
290
298
  | `staysfixed check --selfcheck` | Prove the engine still catches deliberate breakage. |
291
299
  | `staysfixed ship` | The build that went out is now what "working" means. |
292
300
 
301
+ If you have to write a settings block by hand — something `init` could not know, a second
302
+ product, a journey through a screen — every option is in
303
+ [settings.md](settings.md), together with what each kind of product needs installed on the
304
+ machine and the exact command that installs it.
305
+
293
306
  And the three files, if you ever need to look: `src/v2/detect.js` works out what the project
294
307
  is, `src/v2/doctor.js` works out what the machine can do, `src/v2/init.js` turns both into
295
308
  settings and a list of what is left.
package/docs/guards.md CHANGED
@@ -34,8 +34,8 @@ export default {
34
34
  ```
35
35
 
36
36
  They run inside `staysfixed check`, in the same real app the pictures are taken
37
- from. `staysfixed check --guards-only` runs just the guards, which is much faster
38
- and is what you want when your edit could not possibly change how anything looks.
37
+ from. `staysfixed check --guards` runs just the guards, which is much faster and
38
+ is what you want when your edit could not possibly change how anything looks.
39
39
 
40
40
  ---
41
41
 
@@ -126,15 +126,16 @@ 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 **two of those five are written and
130
- not wired.** What a run actually walks today is (1), plus a journeys file you name:
131
- each adapter reads your source and offers what it finds there, and `--journeys
132
- <file>` names steps by hand. The suite harvest and session replay live in
133
- `src/v2/journeys/` with tests around them, and nothing on the check path calls
134
- them; ask for `--journeys suite` or `--journeys recorded` and you are told that by
135
- name rather than handed a clean result about steps something else chose. Saying so
136
- is the point: a feature that exists in the repository and not in the run is not a
137
- feature you have.
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.
138
139
 
139
140
  ---
140
141
 
@@ -343,12 +344,12 @@ release through on the strength of it.
343
344
  | `src/v2/observation.js` | The address space, the channels, `diffCaptures`, `measureWobble`, `subtractWobble`. |
344
345
  | `src/v2/normalise.js` | The rules, as data, each carrying `wouldHide` in plain English, and `explain()` so a normalisation can be answered for. |
345
346
  | `src/v2/cluster.js` | Many differences, one cause, one finding. |
346
- | `src/v2/rank.js` | `rankFindings` — furthest from the edit first, sealed classes above everything — plus `sealOf`, `whatChanged`, `importGraph`. |
347
+ | `src/v2/rank.js` | `rankFindings` — furthest from the edit first, sealed classes above everything — plus `classOf`, `whatChanged`, `importGraph`. Which classes are sealed, and the words used to refuse one, live in `src/v2/sealed.js`. |
347
348
  | `src/v2/cause.js` | `proveCause` — revert the suspect hunk, run again, and find out. |
348
349
  | `src/v2/store.js` | The append-only capture files, build records, references, and reading a torn file without losing the rest. |
349
350
  | `src/v2/run.js` | `runCheck` — the loop, over any `CheckEngine`. |
350
351
  | `src/v2/check.js` | `check` — the assembled front door. The command line, the MCP server and the self-check corpus all look for it here, on purpose: if they found the engine in different places they would be checking different things and reporting it as one. |
351
- | `src/v2/selfcheck.js` | The corpus of deliberately broken builds, and `runSelfcheck`. |
352
+ | `src/v2/selfcheck.js` | The corpus of deliberately broken builds — `CASES` — and `selfcheck`. |
352
353
  | `src/v2/adapters/` | One per surface. The only place that knows what a browser or a child process is. |
353
354
  | `src/v2/mcp/` | The tools an agent calls. |
354
355
  | `src/v2/cli.js` | `V2_COMMANDS`, `run`, `doctorRun`, `checkOptions`, `report`. |
package/docs/mcp.md CHANGED
@@ -91,9 +91,14 @@ Every client that speaks MCP over stdio wants the same three things — a comman
91
91
  its arguments, and somewhere to run it. The JSON block above is the shape almost
92
92
  all of them use; translate it into whatever your client's config file looks like.
93
93
 
94
- `staysfixed doctor --json` returns that block already filled in, under
95
- `wiring.mcp`, and so does `staysfixed_capabilities` with `detail: "full"` so an
96
- agent setting this up for somebody never has to copy it out of this page.
94
+ `staysfixed init --json` returns that block **with the project path already filled
95
+ in**, under `plan.wiring.mcp` so an agent setting this up for somebody can paste
96
+ it straight into the client's config without copying anything out of this page.
97
+
98
+ `staysfixed doctor --json` carries the same block under `wiring.mcp`, but its
99
+ `cwd` is still the placeholder `"/absolute/path/to/your/project"`: `doctor`
100
+ describes the machine rather than the project, so put the project's real path in
101
+ yourself, or take the block from `init --json` instead.
97
102
 
98
103
  ### If you have it installed locally
99
104
 
@@ -168,11 +173,15 @@ stronger, and the right thing before a release), `against` (compare with a named
168
173
  marker or commit), `only` (a list of journey names), `limit` and `offset` (paging
169
174
  through the *last* run without running anything again), and `format: "json"`.
170
175
 
171
- `journeys` names where the steps come from. Today that is the default what each
172
- adapter reads out of your source or a path to a journeys file. `"suite"` and
173
- `"recorded"` are written in `src/v2/journeys/` and not yet wired into a run: ask
174
- for either and you are told so by name, rather than given a clean result about
175
- steps something quietly chose instead.
176
+ `journeys` names where the steps come from. The default is what each adapter reads
177
+ out of your source. `"suite"` walks the project's own test suite as well — each
178
+ test file run twice inside the scratch copy, every check reported by name and why
179
+ each failure failed, stopping after 90 seconds with every file it did not reach
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.
176
185
 
177
186
  ---
178
187