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/CHANGELOG.md CHANGED
@@ -4,6 +4,370 @@ All notable changes to this project are recorded here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the version
5
5
  numbers follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ Nothing yet.
10
+
11
+ ## [0.8.0] — 2026-08-30
12
+
13
+ The night this was pointed at itself. Everything below was found the same way:
14
+ the published build was installed the way a stranger installs it, and used on
15
+ throwaway products — a plain-Node server, an Express API, a Flask app, a static
16
+ site, a Vite app, a bare Gradle project, an empty Xcode project, a CLI that
17
+ reaches the internet three ways — until it said something untrue. Reading the
18
+ code found none of it.
19
+
20
+ **Two of them were the tool lying about itself**, which is the worst thing this
21
+ product can do: it told every agent that asked that it could read web pages and
22
+ then could not open one, and it reported a clean run over an entire uncovered
23
+ HTTP surface. Both are closed.
24
+
25
+ ### Changed
26
+
27
+ - **`staysfixed init` now runs version 2's setup.** It ran version 1's picture
28
+ setup until now. Version 2's `init` had been written and tested and was wired
29
+ to nothing — the command table deliberately left it out, on the grounds that
30
+ somebody might have `staysfixed init` in a setup script and would get a
31
+ different file. True, and not a reason: version 1's `init` writes settings for
32
+ photographing screens, which is not what this tool does any more, so a new
33
+ project got the wrong shape of file and was told it was set up. `init --json`
34
+ now answers with the fields `docs/getting-started.md` tells an agent to read —
35
+ `plan.project`, `plan.readiness`, `plan.needs.person`, `plan.journeys`,
36
+ `plan.covers.short`.
37
+
38
+ - **`staysfixed check --watch` opens version 2's panel, not version 1's.** This
39
+ is the one flag whose meaning moved, and it is written down here because
40
+ nothing else said so. The panel draws what a difference engine finds —
41
+ journeys, wobble, findings, a verdict — where version 1's drew approved
42
+ pictures, which this tool no longer has. `--pictures --watch` and `--guards
43
+ --watch` still open version 1's panel over version 1's run, so the only person
44
+ whose command changed meaning is the one who typed `--watch` on its own.
45
+
46
+ - **The scratch copy is cloned, not copied.** Every run works in a throwaway copy
47
+ of the project so it can write anywhere it likes without touching the real one,
48
+ and that copy used to be made byte by byte. On the project this was built
49
+ against — twelve gigabytes, most of it an iOS build folder — that is twelve
50
+ gigabytes of real disk per build and twenty-four per check, before a single
51
+ journey is walked. It now asks the filesystem to clone instead: on a Mac that is
52
+ one call per file that copies no bytes at all and shares the blocks until
53
+ something writes to them, and on Linux the same where the filesystem supports
54
+ it. Measured on that tree: **41.5 seconds and no bytes moved.** It falls back to
55
+ a real copy wherever cloning is not available, so a filesystem without it is
56
+ slower here and never wrong. Never a symlink and never a hardlink — both point
57
+ back at the real project, which is the one thing the copy exists to protect.
58
+
59
+ ### Added
60
+
61
+ - **`process.skip` — folders to leave out of that copy.** A project can name
62
+ folders that are not worth copying, and a copy that takes over twenty seconds
63
+ now says so and points at the setting. **The bar is deliberately high: only
64
+ things regenerated on demand and read by nothing.** Anything skipped that turns
65
+ out to matter makes a run pass for the wrong reason, and a false pass is the one
66
+ failure this whole tool exists to prevent. It can only ever add to the built-in
67
+ list — `.git`, `.staysfixed`, `.turbo`, `.nyc_output`, `coverage`,
68
+ `.pytest_cache`, `__pycache__`, `.DS_Store` — because a setting that could
69
+ switch one of those back on would only ever make runs slower.
70
+
71
+ - **`docs/settings.md` — every option, and what each surface needs on the machine.**
72
+ None of the roughly ninety settings keys the adapters read was written down
73
+ anywhere: the README said the reference "lives with the code it configures",
74
+ which is no help at all to somebody who is not going to read the code. Nor was
75
+ anything the code needs installed: the exact commands for Xcode, the iOS
76
+ runtime, the Android SDK and its emulator, and the two separate ways a browser
77
+ can be missing were all sitting in the source as `howToGet` strings and in none
78
+ of the documentation. It also names the things that quietly cost you coverage —
79
+ an Android Play Store image, which refuses root forever so the files an app
80
+ writes cannot be seen; a `web.start` or `http.start` that hard-codes its port; a
81
+ Windows desktop nobody is signed in to.
82
+
83
+ - **The README now lists the silences that are still open,** beside the table of
84
+ the ones that were found and closed. A table of closed bugs on its own reads as
85
+ though nothing is left. Twelve rows, each with the file that would close it.
86
+
87
+ - **`staysfixed browsers` is reachable.** It was written, tested, given a finished
88
+ command entry with a comment saying "wiring it up is one line" — and that line
89
+ was never written. README.md told people to run `npx staysfixed browsers` and
90
+ `npx staysfixed browsers --clean` to clear up after an interrupted run, and both
91
+ answered *"There is no command called browsers"*. Somebody whose disk was
92
+ filling with abandoned browser profiles had no way to clear them and no reason
93
+ to doubt the page telling them there was.
94
+
95
+ ### Fixed
96
+
97
+ - **`staysfixed ship --version 0.14.0` printed `0.7.2` and shipped nothing.**
98
+ `--version` is a global flag meaning "print the tool's version", and it is also
99
+ one of `ship`'s own options meaning "the release that went out was called this".
100
+ The two lists were concatenated, the parser reads switches before values, so the
101
+ global one won: the release script got the tool's version number on standard
102
+ output, exit code 0, and no reference cut. A command's own flags now win any name
103
+ they share with a global one.
104
+
105
+ - **`staysfixed walk --no-snap` answered "I do not know the option --no-snap".**
106
+ `--no-snap` — leave both windows where they are instead of putting them side by
107
+ side — was read by `walk` and declared by nobody. It is now in `walk`'s options
108
+ and in its help, so `docs/watching.md` saying both commands take every panel flag
109
+ is true again.
110
+
111
+ - **The very first run on a project answered `ok: true` to a machine.** With no
112
+ build on record there is nothing to compare against, so the run proves nothing.
113
+ It said exactly that, in words, in the summary, and the command line exited 2 on
114
+ it — but `--json` and the MCP reply both carried `ok: true`, with no `blocked`
115
+ and none of the `NOTHING WAS ACTUALLY COMPARED` wording. An agent reads the
116
+ fields, not the sentence, so the two interfaces that matter most reported a pass
117
+ over a run that compared nothing. That is the exact failure this tool exists to
118
+ prevent, produced by the tool itself. `comparedNothing` fired only on
119
+ per-journey gaps, which a true cold start never records; it now fires on an
120
+ empty `reference.id` as well, and the rest of the codebase was swept for the
121
+ same shape — `ok` computed from "were there findings" rather than from "was
122
+ anything compared at all".
123
+
124
+ - **The README said a fresh install pulls two packages under a megabyte.** It
125
+ pulls three: `playwright-core` became a runtime dependency, which is the browser
126
+ driver without a browser in it — about 13MB, and still nothing that takes
127
+ minutes. The install section now says what actually happens, including that a
128
+ browser already on the machine is found and used before anything is downloaded,
129
+ and that a project already using the full `playwright` is never asked for a
130
+ second copy of the same driver.
131
+
132
+ - **`staysfixed check --guards-only` does not exist and never did.**
133
+ `docs/guards.md` had told people to run it since it was written. The flag is
134
+ `--guards`.
135
+
136
+ - **`staysfixed rules` and `staysfixed browsers` were both named in the README and
137
+ neither existed.** `browsers` has been wired up; there is no `rules` command, so
138
+ the README now says where the rules actually are — one file,
139
+ `.staysfixed/rules.json`, read like any other file in a pull request.
140
+
141
+ - **`--watch-width 100` is refused, not quietly widened.** `docs/watching.md` said
142
+ anything under 240 or over 900 was brought back to the nearest of the two. Under
143
+ 240 is an error with a message; over 900 is brought down.
144
+
145
+ - **`--profile` prints nothing on a difference-engine check.** It works on
146
+ `staysfixed walk` and on the version 1 picture check. The flag is accepted by the
147
+ new check and the timings are not kept, so `docs/watching.md` no longer claims
148
+ otherwise.
149
+
150
+ - **`staysfixed doctor --json` does not fill the project path into its MCP wiring
151
+ block** — `staysfixed init --json` does. `docs/mcp.md` had said both did.
152
+
153
+ - **`docs/design-v2.md` named four tools that were never used.** It is a plan
154
+ rather than a description and says so at the top, but it is also the only
155
+ per-platform prose in the repository, and it told anybody reading it that
156
+ Android needs Appium, UiAutomator2 and a Java runtime, iOS needs WebDriverAgent
157
+ and Windows needs a .NET probe built on FlaUI. None of those is used. What
158
+ shipped needs none of them, and the section now says so before the list rather
159
+ than after it.
160
+
161
+ - **`--color` was accepted by every command and did nothing.** Colour is settled
162
+ before anything else loads, so by the time a command is parsed the answer is
163
+ already fixed and nothing on the command line could turn it back on. `--no-color`
164
+ works and always did — it is handled first thing, before the logging module
165
+ decides. `--color` has been taken away rather than left as a switch that turns
166
+ nothing on.
167
+
168
+ ### Added — products it could not see before
169
+
170
+ - **A hand-written `node:http` server is read.** A product with `createServer`, a
171
+ chain of `req.url === '/x'` and `.listen(PORT)` was reported as "one thing: the
172
+ `notes` command", found zero routes, left the `http` block commented out, and
173
+ said **"a check here covers the `notes` command in full. Nothing is being left
174
+ out."** over an entire uncovered surface. The same product written with Express
175
+ was handled perfectly, so the machinery worked and the reading stopped at
176
+ frameworks. Eight hand-rolled routing shapes are now read — `req.url === '/x'`,
177
+ `.startsWith`, a `switch` on a pathname, `new URL(req.url, base).pathname`,
178
+ `url.parse`, the `split('?')[0]` variant, `if`/`else if` chains, and a path held
179
+ in a constant — and a method is claimed only where the code checks one, `ANY`
180
+ otherwise, never `GET`.
181
+
182
+ - **Python products are checkable.** A Flask app was told "a Python project is not
183
+ being checked… in a language nothing here drives". The honesty was right and the
184
+ conclusion was wrong: the two adapters that would run it read no source at all.
185
+ The process adapter runs a command and compares what it printed, exited with and
186
+ touched; the http adapter boots a server and asks it for routes. Flask, FastAPI
187
+ (including an `APIRouter` prefix) and Django `urlpatterns` are now read, and
188
+ their path parameters become the same "somebody has to give a real value" flow
189
+ every other route uses. Go, Rust, Ruby and PHP get the boot and the command
190
+ without the routes — and every one of them says, **by name**, that the source
191
+ channel is blind for that language, so none can reach "covered in full".
192
+
193
+ - **`--journeys suite` walks the project's own test suite.** It was written,
194
+ tested and reachable by nothing. Each test file runs twice inside the scratch
195
+ copy with the same frozen clock, seed and watcher as everything else, and every
196
+ check is reported by name, along with why each failure failed and a fingerprint
197
+ of the test file itself — so an edited test says plainly that the change is
198
+ yours. It earns its keep: take the penny-rounding out of a `total()` and the
199
+ product's own output does not move by one character, the discovered journeys say
200
+ nothing has changed, and the harvest names the check that turned red. Off unless
201
+ asked for, held to `suite: { budgetMs }` (90 seconds by default, `0` for none),
202
+ and **every file the budget did not reach is named one by one** rather than
203
+ summarised as "some tests were skipped".
204
+
205
+ - **`keepBuilds` — how much of the record is kept.** The store grew for ever: one
206
+ build folder per commit, in your git history, with the function that would have
207
+ stopped it written and called by nothing. Three tiers now, capped by count and
208
+ not by age, and it refuses to remove anything at all if any record could not be
209
+ read.
210
+
211
+ ### Fixed — the tool being wrong about itself
212
+
213
+ - **It could not open a single web page, and said it could.** `playwright` was
214
+ dropped from the dependencies on the reasonable-looking grounds that nothing
215
+ under `src/` imported it. Nothing does — the web adapter reaches it through
216
+ `await import()`, which no search for an import statement can see. So every
217
+ website check on a fresh install answered "Playwright is not installed, so no
218
+ web page can be opened", while `staysfixed_capabilities` told the calling agent
219
+ that web apps and sites could be checked *here and now*. The dependency is back
220
+ as `playwright-core` — the driver without a browser in it, 13MB and no download,
221
+ because this tool already knows how to find Chrome for Testing, Chrome, Edge or
222
+ Chromium and deliberately prefers one that is not the browser you use. A test
223
+ holds it in place by name, and holds `doctor` and the walk to the same answer.
224
+
225
+ - **An Android project with no APK built was reported as covered in full,** and
226
+ then could not be walked at all. iOS already named the missing build and the
227
+ exact `xcodebuild` line; Android now names the Gradle one. A Tauri app read as
228
+ ready on any machine with no Windows host, because `doctor` returns no Windows
229
+ needs precisely when there is no Windows to ask about.
230
+
231
+ - **Version 1 passed a page with a letter missing from its heading.** The picture
232
+ check's default allowed 0.05% of a picture through, described in a comment as
233
+ "enough for font hinting noise, nowhere near enough to hide a missing
234
+ stylesheet". On a 2880×1800 screenshot that is **2,592 pixels**; taking one
235
+ letter out of an `h1` moves **593**. The replacement was measured rather than
236
+ chosen — ten fresh takes of the same build differ by **zero** pixels — so the
237
+ default is now zero, and a project that sets `tolerance.pixels` is told, per
238
+ screen, how many pixels its setting just swallowed.
239
+
240
+ - **The network boundary let everything out.** The watcher injected into every run
241
+ read the host off the first argument, but Node normalises `Socket.connect` to
242
+ `[options, callback]` — so the host read as an empty string, which was in the
243
+ loopback allow-list. `fetch`, `http.get` and `net.connect` to the open internet
244
+ all succeeded and the report came back empty, while the tool's loudest safety
245
+ claim said every outbound connection had been refused. All three are now refused
246
+ and recorded by host and port.
247
+
248
+ - **`npm run start` died silently** — the default start command `init` writes for
249
+ every Node server. The watcher wrapped `process.env` in a Proxy with only `get`
250
+ and `has` traps, so `process.env.X = 'y'` quietly failed, npm could not pass
251
+ `npm_lifecycle_event` to the script, and it exited 1 without printing a word.
252
+ Every trap now forwards through `Reflect`, with `set` taking the target as the
253
+ receiver.
254
+
255
+ - **Every route, and every command, read as never walked.** A door is addressed
256
+ `route.<VERB>.<url>` and `cli.<the command>`; the adapters wrote `api.<journey>`
257
+ and `cli.<journey name>`. So the coverage ledger — the one part of this tool
258
+ that must never be optimistic — was instead permanently and wrongly pessimistic,
259
+ and a genuinely uncovered door was invisible in the noise.
260
+
261
+ - **A normalisation-rules caveat fired on every run, for ever.** A per-run
262
+ temporary folder was baked into a rule pattern and collided with the real
263
+ project-root rule, so the rule set contained a fresh random pattern every run —
264
+ and the real rule was silently deleted, meaning absolute paths under your
265
+ checkout were never normalised at all.
266
+
267
+ - **A committed change could not be ranked.** "The change" meant the working tree
268
+ and nothing else, so the moment an agent committed its work — which is what an
269
+ agent does at the end of a task — the distance measure went blind and every
270
+ finding carried "nothing in the working tree has changed, so there is no edit to
271
+ measure this against" over a change that was perfectly well known.
272
+
273
+ - **The scratch copy copied every byte.** On the project this was built against —
274
+ twelve gigabytes, most of it an iOS build folder — that is twelve gigabytes of
275
+ real disk per build and twenty-four per check. It now asks the filesystem to
276
+ clone: **41.5 seconds and no bytes moved**, measured on that tree. Never a
277
+ symlink and never a hardlink, because both point back at the project this exists
278
+ to protect.
279
+
280
+ ### Fixed — caps that decided things quietly
281
+
282
+ - **A finding's nearby files were cut to five, and the sealed classes are searched
283
+ in that list.** A finding whose sixth file was `src/billing/refund.js` was
284
+ classified `ordinary` — precisely the class an agent may close on its own.
285
+
286
+ - **A waiver was pinned to the first 40 differences and 20 addresses of a
287
+ finding.** A waiver written over a three-hundred-address finding went on
288
+ covering it after the value at address 41, 150 or 299 became something else.
289
+
290
+ - **A wobble storm needed twelve addresses before it counted,** so ten of eleven
291
+ wobbling was not a storm: ten real differences subtracted, and the run passed.
292
+
293
+ - **Eight address truncations with no fingerprint,** across four files. Two long
294
+ labels, two journey names, two log lines or two checkpoints that agreed for the
295
+ first 60 to 200 characters landed on one address and one of the two answers was
296
+ thrown away at the door. Two of them were screenshot filenames, so the picture
297
+ offered as evidence for a finding could be a photograph of a different screen.
298
+
299
+ - **`trimForStorage` cut in characters against a byte limit.** On box-drawing or
300
+ CJK output, 90,000 bytes in produced 180,061 bytes stored — the whole text twice
301
+ — under a marker claiming 24,464 bytes had been left out of the middle.
302
+
303
+ - **Files over 8MB were compared by a size bracket, not a fingerprint,** so a
304
+ build that wrote a different 40MB file compared equal. They are stream-hashed
305
+ now, and the remaining ceiling is named in the ledger.
306
+
307
+ - **An adapter that did not answer in time made its surface READY.** The race
308
+ resolved an empty needs list, and no needs meant covered — so an Android adapter
309
+ hanging for sixteen seconds produced "Covered against the stored record."
310
+
311
+ ### Changed — words a person reads
312
+
313
+ - **The watch window opened on a fifteen-line paragraph.** Its headline was right
314
+ and the engine's entire summary sat underneath it as one grey block. Nothing is
315
+ dropped — the summary is split at sentence ends, two are shown, and the rest sit
316
+ behind one control.
317
+
318
+ - **A finding titled with the tool's own word.** A renamed route read as
319
+ `"declared" is gone`. Twenty-seven such words are now recognised, so it reads
320
+ `"/notes / declared" is gone` instead.
321
+
322
+ - **A sentence that said a thing was now what it already was.** When both sides of
323
+ a change summarised to the same words, the summary was the wrong thing to print.
324
+ The flagship case now reads: *"GET /invoice / body" now has "line" reading "A
325
+ desk lamp ......... 49.99 GBP" where it read "A desk lamp ......... £49.99"*.
326
+
327
+ - **The first error anybody meets writing a guard was raw JavaScript** —
328
+ `page.goto is not a function`. It now says what the object it was handed
329
+ actually has, in one sentence, and suggests a name only when exactly one is
330
+ close.
331
+
332
+ - **The shipped examples were for a shape the tool no longer uses.** Both said
333
+ "copy this to the root of your project"; doing that today gets you a settings
334
+ file the difference engine rejects. Each now says which half it belongs to, and
335
+ points at `staysfixed init` — which beats any example, because it reads the
336
+ project in front of it.
337
+
338
+ ### Removed
339
+
340
+ - **Fourteen exports nothing called.** Ten were genuinely dead. Two were missing
341
+ features and are now wired: `treeMovedSince`, the one thing `intent.js` promised
342
+ and nothing checked, and `RUNNER_KINDS`, which validated nothing while an unknown
343
+ runner kind was silently treated as posix. Two were kept and wired because
344
+ deleting them would have quietly broken a promise the documentation makes.
345
+
346
+ - **`ios.suite` was advertised in a message telling somebody how to switch
347
+ something on.** Nothing anywhere read it.
348
+
349
+ ## [0.7.2] — 2026-08-30
350
+
351
+ Found the same way as 0.7.1, one step further along: install from npm into an
352
+ empty folder, run `init`, then run `doctor` on what `init` just wrote.
353
+
354
+ ### Fixed
355
+
356
+ - **`doctor` read the commented-out examples in a settings file as settings.**
357
+ `staysfixed init` comments out every option that does not apply to your
358
+ project rather than leaving it out, so nothing is hidden from the person
359
+ reading the file — and `doctor` searched the raw text. On a folder holding one
360
+ script and nothing else it announced *"Electron desktop apps: **Covered.** It
361
+ opens release/mac-arm64/Your App.app"*, and an Android app beside it, both read
362
+ out of comments and both false.
363
+
364
+ A surface reported as covered when nothing will ever be walked on it is the
365
+ worst answer this tool can give, because every clean result after it is
366
+ believed. Comments are taken away before anything is read out of the file now,
367
+ with strings respected so an address keeps its two slashes — and the file is
368
+ still never loaded, because a settings file may be JavaScript and doctor must
369
+ not run somebody's code to answer a question about their machine.
370
+
7
371
  ## [0.7.1] — 2026-08-30
8
372
 
9
373
  Found by installing 0.7.0 from npm into an empty folder and running it the way a