saffron-ai 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,270 @@ All notable changes to Saffron, in one place: the `saffron-ai` runner (npm),
4
4
  the VS Code extension (`ChathurangaJayasinghe.saffron-vscode`) and the
5
5
  JetBrains plugin (`ai.saffron.jetbrains`). Dates are publish dates.
6
6
 
7
+ ## 0.7.1
8
+
9
+ Released 2026-09-20.
10
+
11
+ Runner
12
+
13
+ - **`expectUrl` checks the active page, not any open tab.** A URL sitting in
14
+ a leftover tab could satisfy the assertion for the wrong reason. *A
15
+ recording made before tab openers were marked, with a slow popup and
16
+ `retries: 0`, can now fail where it used to pass: re-record it, or leave
17
+ the default retry, which re-follows the newest tab.*
18
+ - **A changed table value only replays if the recording really uses it.**
19
+ Matching key sets used to be enough, so a recording that still carried the
20
+ old value as a literal, or never referenced the cell, stayed "valid" and
21
+ typed stale data. The old value is looked for only where a value can live
22
+ (typed, matched or named text, with tokens removed), so an empty cell or a
23
+ short value like "on" does not force a needless re-record.
24
+ - **Propagation never rewrites an assertion**, on either side: assertion
25
+ steps and assertion actions are skipped when a fix is extracted and when
26
+ it is applied, the step wording must match, and a locator inside a frame
27
+ is a different locator. When one heal sends the same locator to more than
28
+ one place, nothing propagates and `saffron accept` says so.
29
+ - **`saffron prune` and `saffron status` respect features outside the
30
+ configured directory.** A scenario run as `saffron run e2e/login.feature`
31
+ had its recordings listed as orphans. Feature files named by existing
32
+ recordings are now inspected too, and one that fails to parse blocks the
33
+ prune like any other.
34
+ - **Step reuse no longer brings back a stale table value.** Changing `alice`
35
+ to `bob` correctly invalidated the scenario's cache, and step reuse then
36
+ seeded the same actions straight back, matching on table keys alone: a
37
+ verified proposal at zero AI calls whose table said `bob` while its
38
+ actions still typed and asserted `alice`. Seeds now pass the same rule as
39
+ cache validation, and a step with no table of its own is refused when it
40
+ carries another step's old value as a literal. The old value is looked
41
+ for in locators built from data as well (a test id like `user-alice`,
42
+ fallback selectors, a frame, a response URL pattern), not only in typed
43
+ and named text: a fill can be parameterized while the assertion beside it
44
+ still targets the old user.
45
+ - **A feature edit cannot land on the wrong step.** Edits address steps by
46
+ index, so a step inserted while a proposal waited shifted every index
47
+ after it, and an edit meant for "I click login" rewrote the new step.
48
+ `saffron accept --with-feature-edit` now compares the scenario with the
49
+ steps the proposal was recorded against and refuses before anything is
50
+ consumed: the file, the cache and the proposal stay as they were.
51
+ - **A skipped feature edit no longer reaches the cache.** When one edit
52
+ applied, the accepted cache was given the new text of every edit,
53
+ including a Background edit that was correctly skipped, so the feature and
54
+ its cache disagreed at once. Only edits that landed are synced. Two edits
55
+ that reach the same StepSet line through two invocations are applied once
56
+ when they agree, and both refused when they do not, naming the two
57
+ wordings: the line can only say one thing.
58
+ - `saffron status` parses each feature file once. The vocabulary and the
59
+ orphan scan each parsed the whole project again; both now reuse what
60
+ status already read. IDE panels call status on every refresh.
61
+
62
+ Editors (VS Code extension 0.2.5)
63
+
64
+ - **Completion badges and duplicate-step diagnostics follow cache creation
65
+ and deletion.** Only changes to existing JSON files were watched, so
66
+ accepting a first recording or pruning one left the vocabulary stale until
67
+ something else triggered a rebuild. Recordings are watched for all three
68
+ events (and only recordings, not every JSON file), and the watcher is
69
+ disposed with the extension.
70
+ - **Vocabulary is per project in a multi-root workspace.** Step sets,
71
+ wording and recorded badges were built once for the whole workspace, so
72
+ project B's `Login` step set could answer go-to-definition in project A,
73
+ and a step recorded in A showed as recorded in B.
74
+ - **The inline diff button on a proposal row works.** An inline action
75
+ passes the tree item, not the file path the row click passes; both forms
76
+ are accepted, and the project guard applies to both.
77
+
78
+ ## 0.7.0
79
+
80
+ Released 2026-09-19.
81
+
82
+ New: `saffron diff`, `saffron prune`, a configurable `pollIntervalMs`, and a
83
+ configuration reference in the docs. The rest of this release is correctness
84
+ work from several rounds of external review.
85
+
86
+ **Upgrading.** Three fixes can change what an existing suite does, all in
87
+ the direction of telling the truth:
88
+
89
+ - A scenario that was green because replay quietly clicked a near-match
90
+ ("Delete all orders" for a missing "Delete"), or because a `Then` step
91
+ never checked anything, now fails and heals or asks to be re-recorded.
92
+ - Feature files in subdirectories get a new cache directory name (a short
93
+ digest of their path is appended), so their scenarios record again once.
94
+ Files directly under the features root are unaffected. `saffron prune`
95
+ lists the old directories afterwards.
96
+ - `saffron status --json` gains `cacheState` and `orphans`; nothing was
97
+ removed.
98
+
99
+ Runner
100
+
101
+ Correctness fixes from an external review of the runner. Four of these
102
+ could let a run look green or yellow when the goal was never checked.
103
+
104
+ - **Secrets could reach your feature files.** Machine-applicable feature
105
+ edits were the one agent output that skipped `{env:VAR}` masking, so
106
+ `saffron accept --with-feature-edit` could write a resolved password into
107
+ a scenario. Feature edits, the narrative and the suggested edit are now
108
+ masked everywhere they are stored or reported.
109
+ - **An assertion that recorded nothing now fails.** A `Then` step with an
110
+ empty action list used to replay as a pass, so a scenario could be green
111
+ without its verdict ever being checked. Replay fails such a step, and the
112
+ recording is filed unverified. *This can turn an existing green scenario
113
+ red: that assertion was never being checked. Re-record it.*
114
+ - **Negative assertions consider every recorded locator.** `expectNotVisible`
115
+ returned as soon as one candidate was hidden, so a real failure could hide
116
+ behind a missing primary locator while a fallback was visible.
117
+ - **Recorded names match exactly first.** A step recorded against "Save"
118
+ could click "Save draft" when it came first in the DOM. Substring matching
119
+ remains the fallback for reading the page. The agent is no longer told to
120
+ shorten names.
121
+ - **Cache files are unique per scenario.** The cache directory now includes
122
+ the feature file's directories, so two same-named files in different
123
+ folders stop overwriting each other's recordings. Flat layouts are
124
+ unchanged. A run refuses to start if two scenarios would still share a file.
125
+ - **Suspicious recordings are saved unverified.** The check ran after the
126
+ proposal was written, so the file said verified while the run said
127
+ otherwise, and `accept --all` took it.
128
+ - **An unverified proposal shows the proof replay's timeline**, not the
129
+ agent session's, so the report says which step could not replay instead
130
+ of marking every step passed.
131
+ - **A step that changes between an action and an assertion is stale.** Cache
132
+ matching compared the step's words but not its keyword, so turning
133
+ `When I see the total` into `Then I see the total` kept the old recording
134
+ and replayed the action instead of checking anything.
135
+ - **Waiting for a new tab happens where a tab opens.** Every click paid a
136
+ 150ms wait, which taxed scenarios that never use tabs and still lost the
137
+ race with a slower popup. The proof replay now records which action opens
138
+ a tab, and only that action waits, for as long as any other action may
139
+ take. A five-step login scenario replays in about a third of the time.
140
+ - **`saffron status` distinguishes a valid cache from a stale one.** It
141
+ reported `cached` by asking whether a file existed, so a scenario whose
142
+ text had changed still looked recorded. Each scenario now carries
143
+ `cacheState`: `valid`, `stale`, `invalid` or `missing`, and the printed
144
+ view marks stale ones.
145
+ - **`saffron lsp` no longer reports a valid step set as defined twice** in
146
+ projects whose path contains a space: it compared a file system path with
147
+ a percent-encoded URL path.
148
+
149
+ - **`saffron diff` shows what a proposal changes**: the committed recording
150
+ against the proposed one, action by action, with a rewritten action shown
151
+ as one before-and-after pair and changed assertions counted separately.
152
+ The narrative is the agent's account of what it did; this is what a
153
+ reviewer is approving. It also appears in the HTML report and in both IDE
154
+ panels.
155
+ - **An assertion step must contain an assertion.** The earlier guard only
156
+ rejected an empty action list, so a `Then` that recorded a `waitFor` and
157
+ nothing else still passed.
158
+ - **A control that is found but cannot be used no longer hands the action to
159
+ a different control.** With a disabled "Save" next to an enabled "Save
160
+ draft", replay clicked the draft button. An interaction now stops at the
161
+ candidate that matched, so drift goes to the failure and healing path
162
+ instead of quietly doing something else.
163
+ - **Negative assertions check every match of every locator.** Checking only
164
+ the first match let a hidden first element vouch for a visible second one.
165
+ A locator that errors no longer counts as proof of absence.
166
+ - **Secrets are discovered through Examples rows.** A `{env:VAR}` reaching a
167
+ table cell or doc string through a placeholder was invisible to masking,
168
+ so it could survive into recordings, reports and feature edits.
169
+ - **Cache identity follows the path, not its spelling.** `admin/login.saffron`
170
+ and `admin-login.saffron` produced the same cache directory; nested files
171
+ now carry a digest of their path. The collision check also looks at the
172
+ whole project, because two features run separately never met before.
173
+ - **A recording that says a tab opens must see one.** The marker was
174
+ advisory: when no tab appeared the timeout was swallowed and the next
175
+ assertion ran against the old page. Tab openers are also captured while
176
+ recording now, not only during the proof replay, which could miss them.
177
+ - **An interaction never substitutes a different control.** A recorded name
178
+ that no longer matches exactly could still act on any control containing
179
+ it, so a missing "Delete" could click "Delete all orders" and the run
180
+ stayed green. Interactions now require the exact name; assertions and
181
+ captures keep the substring fallback, because reading the page is not
182
+ acting on it. *A scenario whose control was renamed now fails and heals
183
+ instead of passing.*
184
+ - **The proposal diff compares actions, not their descriptions.** Two
185
+ actions that read the same could differ where it counts: dropping a
186
+ response body check or repointing a selector from `#save` to `#delete`
187
+ both reported no change. Actions are compared field by field, and a
188
+ rewritten action now lists exactly which fields moved.
189
+ - **Repeated steps stay separate in the diff.** Steps were matched by their
190
+ wording, so a scenario that adds the same item twice showed no change when
191
+ the proposal dropped one of them.
192
+ - **A popup that arrives late is pinned on the action that opened it.** The
193
+ opener marker waited for the next recorded action, so a slow window was
194
+ blamed on whatever the agent did afterwards, and replay then waited for a
195
+ tab that action never opens. Only an interaction can be credited: a wait
196
+ or an assertion recorded between the click and the popup no longer takes
197
+ it, and the marker now follows the agent's actual tool calls instead of
198
+ inferring them. Any action marked as opening a tab waits for one, not
199
+ only clicks. A popup nobody can be credited with is dropped rather than
200
+ passed to a later action: one that arrives while the agent is idle and
201
+ the last interaction is too old, one that lands during a call that failed
202
+ or could have opened it itself, and one the agent opened with a new tab.
203
+ Only known observation tools (a snapshot, a wait, listing tabs) may vouch
204
+ for the click before them, and failed tool calls are now drained so they
205
+ cannot hold an attribution open.
206
+ - **`saffron prune` removes recordings nothing owns.** Deleting a scenario
207
+ or a feature file left its cache behind, where it stayed in git and no
208
+ reviewer could tell it from a live one. `saffron prune` lists every
209
+ orphaned cache and proposal with the reason, deletes nothing without
210
+ `--yes`, and `--check` exits 1 for CI. Ownership follows the path, so a
211
+ stale or unreadable cache that a live scenario claims is kept, and while
212
+ any feature file fails to parse prune refuses to judge anything.
213
+ `saffron status --json` carries the same list for IDE panels.
214
+ - **`pollIntervalMs` is configurable** (default 100ms, minimum 10). It sets
215
+ how often a polled assertion re-checks the page inside `actionTimeoutMs`:
216
+ lower reacts sooner, higher costs less on a busy CI box.
217
+
218
+ Docs
219
+
220
+ - **A configuration reference**, grouped by what you are tuning (timing,
221
+ project, runtime, the agent, gates) with every option's default, replacing
222
+ the paragraph that listed the keys in prose. It ends with how timing
223
+ actually works: Playwright's actionability wait, then polled assertions,
224
+ then action retries, and why there is no whole-scenario retry.
225
+
226
+ Editors (JetBrains plugin 0.2.2, VS Code extension 0.2.4)
227
+
228
+ - JetBrains plugin: **Accept Selected with nothing ticked accepted every
229
+ proposal.** Blank paths mean `--all` to the runner, so the deliberate
230
+ review workflow could be bypassed by clicking the wrong button. It now
231
+ does nothing and says so; bulk acceptance stays its own action.
232
+ - Both IDEs list **recordings no scenario owns any more**: a JetBrains
233
+ *Orphans* tab (grouped by cache and proposal, double-click to open one,
234
+ *Remove All* after a confirmation) and a VS Code *Orphaned recordings*
235
+ section with the same action in its title bar. Both name the reason, and
236
+ both say plainly when the installed runner is too old to report them
237
+ rather than claiming there are none. `prune` is also a JetBrains run
238
+ configuration command, where listing is the default.
239
+ - JetBrains plugin: **the Proposals tab could show an old diff beside a new
240
+ narrative.** Diffs were cached by file name, and a new run overwrites a
241
+ proposal under the same name, so Accept could promote something the
242
+ reviewer never read. A refresh now invalidates every diff, a late result
243
+ from before the refresh is discarded, and the cache lives on the UI thread.
244
+ - JetBrains plugin: **a path with a space stays one path.**
245
+ `features/order checkout.saffron` became two arguments, even for
246
+ right-clicked and ticked files. Paths are parsed like a command line, and
247
+ what the plugin writes itself is quoted to match.
248
+ - VS Code extension: **Run selected ran project B's files from project A**
249
+ in a multi-root workspace, with A's config and install. Selections are now
250
+ grouped by owning folder, one run per project, from the Explorer too. The
251
+ status-backed sections name the project they show, and *Select project*
252
+ switches it, instead of silently using the first folder. Switching
253
+ clears those sections at once rather than when the new status arrives:
254
+ rows left on screen stayed clickable, so a proposal ticked in project A
255
+ could be accepted in project B when both had the same relative file.
256
+ Ticks are bound to the project that listed them, and accept, reject,
257
+ run tagged and prune refuse to act while a status is still loading.
258
+ - VS Code extension: **the proposal diff could not start on Windows**, where
259
+ Node refuses to launch a `.cmd` without a shell. Status and diff now share
260
+ one launcher: the installed CLI runs under node with a plain argument
261
+ array, and only the npx fallback goes through cmd.exe, fully escaped.
262
+ - Both IDEs show the **proposal diff**: JetBrains under the narrative in the
263
+ Proposals tab, VS Code by opening the diff as a document when a proposal is
264
+ clicked or from its inline action.
265
+ - VS Code extension: runs the CLI as a **task with an argument array and the
266
+ owning folder's working directory**, instead of writing a command line
267
+ into a shared terminal. A file name with a dollar sign or a backtick is no
268
+ longer shell syntax, and a file from a second workspace folder runs
269
+ against that folder's config and install.
270
+
7
271
  ## 0.6.0
8
272
 
9
273
  Released 2026-09-18.
@@ -20,6 +284,12 @@ Runner
20
284
  - When a pending proposal fails its zero-AI replay and the agent records
21
285
  again, the run now says which step failed and why.
22
286
 
287
+ Editors
288
+
289
+ - VS Code extension 0.2.3: the Marketplace listing is named "Saffron",
290
+ its short description fits without being cut off, and the overview
291
+ introduces the tool and how to get started before listing features.
292
+
23
293
  ## 0.5.7
24
294
 
25
295
  Released 2026-09-17.
package/README.md CHANGED
@@ -90,6 +90,7 @@ Configuration lives in `saffron.config.json` at your project root:
90
90
  "baseURL": "http://localhost:4173",
91
91
  "features": "features",
92
92
  "actionTimeoutMs": 5000,
93
+ "pollIntervalMs": 100,
93
94
  "retries": 1,
94
95
  "model": "claude-sonnet-5",
95
96
  "maxTurns": 100,