isomorphic-jj 1.4.3 → 1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,428 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-09-02
4
+
5
+ The first `@johnhenry/isomorphic-jj` release since 0.1.0, and the first
6
+ scoped release to carry the audit fixes from PR #19 below — **the published
7
+ 0.1.0 tarball on npm predates #19** and does not include them.
8
+
9
+ ### Fixed (PR #19 — audit findings)
10
+
11
+ Fixes all 8 findings from a bug-hunt audit of the storage/operation-log,
12
+ undo/redo, revset, and Git-push layers:
13
+
14
+ - **Same-process storage locking.** `Storage.appendLine()`/`write()` did
15
+ read-then-write with no lock, so concurrent same-process calls (e.g. a
16
+ user command racing `BackgroundOps`'s autosnapshot) could silently drop an
17
+ entry. Added a path-keyed in-process async mutex (`src/utils/mutex.js`)
18
+ that serializes the full critical section for both methods. Scoped to
19
+ same-process concurrency per the audit's guidance; cross-process locking
20
+ is a follow-up.
21
+ - **`undo()` now reverts in-place content mutations.** Previously it never
22
+ restored `ChangeGraph`/tag mutations made in place. `describe()` now
23
+ snapshots the affected `ChangeGraph` node before mutating it (mirroring
24
+ the existing `fileSnapshot` pattern), and `undo()` restores it.
25
+ `operations.revert()` now also diffs/restores `tags`, not just
26
+ `bookmarks`.
27
+ - **Ambiguous change-id prefixes now throw `AMBIGUOUS_ID`.** `show()`
28
+ previously resolved an ambiguous prefix silently to the first match; it
29
+ now throws `AMBIGUOUS_ID` listing all matches.
30
+ - **`push()` surfaces categorized error codes.** Previously all per-ref
31
+ errors were swallowed into a bare ref name. Rejected refs now carry a
32
+ categorized `code` (`AUTH_FAILED`/`NETWORK_ERROR`/`NON_FAST_FORWARD`/
33
+ `OTHER`) and `reason` so callers can tell an auth failure apart from a
34
+ non-fast-forward rejection instead of guessing.
35
+ - **Conflict-marker newline fix.** Markers could fuse content onto boundary
36
+ markers when content lacked a trailing newline; now forces a newline
37
+ separator for non-empty content.
38
+ - **Atomic protobuf writes.** `jj-operation-store.js`, `jj-view-store.js`,
39
+ and `jj-tree-state.js` risked a truncated/undecodable file on a mid-write
40
+ crash. Now route through a shared `atomicWriteFile()` temp+rename helper
41
+ (`src/utils/atomic-write.js`).
42
+ - **Weak temp-file naming fixed alongside storage locking** by adding a
43
+ random suffix, so two concurrent writes can't collide on the same temp
44
+ filename.
45
+ - **Open-ended revset ranges** (`..b`, `a..`, `::`) failed with a
46
+ misleading "invalid IDs" error; they now delegate to the existing
47
+ `ancestors()`/`descendants()`/`all()` implementations.
48
+
49
+ Closes #11–#18. See [PR #19](https://github.com/johnhenry/isomorphic-jj/pull/19).
50
+
51
+ ### CI (PR #20)
52
+
53
+ - Dropped macOS from the test matrix — hosted macOS runners are scarce/slow
54
+ to schedule and this is a pure-JS library with no native code path; Linux
55
+ + Windows already cover realistic cross-platform risk. See
56
+ [PR #20](https://github.com/johnhenry/isomorphic-jj/pull/20).
57
+
58
+ ### Documentation overhaul: examples suite
59
+
60
+ - **New `examples/` directory: 12 numbered, runnable examples** covering the
61
+ core loop (init/describe/new/log), stable-ID stacked changes, history
62
+ editing (split/squash/abandon/duplicate), branching and merging (including
63
+ `new({ parents })` for true merge changes), first-class conflicts, revsets,
64
+ bookmarks + tags (with the v0.44 `tag.track()` surface), Git interop,
65
+ configuration layers, undo/oplog/time travel, the `file.*` namespace, and
66
+ browser usage. Each example runs in a temp directory, cleans up after
67
+ itself, and *asserts* what it demonstrates, so the set doubles as a smoke
68
+ test. `npm run examples` runs the loop; `npm run example:NN` runs one.
69
+ - **Example 08 exercises real `git.clone()`/`git.push()`/`git.fetch()` with
70
+ zero network**: it builds a bare fixture repo with the git CLI and serves
71
+ it on 127.0.0.1 through `git http-backend` (buffering request bodies so
72
+ CGI gets a `CONTENT_LENGTH` — isomorphic-git sends chunked bodies). Skips
73
+ cleanly where the git CLI is unavailable.
74
+ - **CI**: the test workflow now runs the examples loop as a smoke step on
75
+ one matrix leg (ubuntu-latest / Node 22), deliberately a different leg
76
+ than the coverage gate.
77
+ - Companion docs for the site (opensource.johnhenry.me) were written from
78
+ this repo's README/API/MIGRATION/CONFIGURATION material in the same pass;
79
+ behavioral quirks the examples surfaced (e.g. `file()` matching snapshots
80
+ rather than modifications, duration revsets needing committer timestamps,
81
+ `init()` after `git.clone()` re-rooting `refs/heads/main`) are documented
82
+ there rather than papered over.
83
+
84
+ ### Publishing
85
+
86
+ - `@johnhenry/isomorphic-jj@0.2.0` is published from `main`. A final
87
+ unscoped bridge release, `isomorphic-jj@1.8.0` (same code as this
88
+ release), will follow so existing unscoped installs get one more version
89
+ pointing at the new package before the unscoped name is deprecated.
90
+
91
+ ## 0.1.0 — Track jj through v0.44
92
+
93
+ Jujutsu shipped v0.44.0 (2026-08-05) since isomorphic-jj's last parity pass
94
+ (v1.5.0/README, which tracked jj through v0.43). This release closes the gap
95
+ for the parts of v0.43–v0.44 that are in scope for a headless, browser-safe
96
+ reimplementation of jj's model — a library, not the `jj` CLI's terminal UX or
97
+ its Rust process/Git-plumbing internals.
98
+
99
+ ### Added
100
+
101
+ - **`tag.track({ name, remote })` / `tag.untrack({ name })`** — jj v0.44 made
102
+ tags remote-trackable the same way bookmarks already were, and added `jj
103
+ tag track`/`untrack` to manage that per-tag. `TagStore` now persists
104
+ tracking state (`{ remote, remoteName }` per local tag name) the same way
105
+ `BookmarkStore` already did for bookmarks, and `tag.list()` includes a
106
+ `tracking: { remote, ref }` field once a tag is tracked. The on-disk
107
+ `tags.json` format gained a `{ tags, tracked }` envelope; the old flat
108
+ `{ name: changeId }` format is still read for backward compatibility.
109
+ - **`builtin_log()` revset function** — jj v0.44 added `builtin_log()` as an
110
+ alias for the revset the built-in `jj log` uses by default, so a custom
111
+ `revsets.log` config can extend it instead of duplicating it. isomorphic-jj's
112
+ `log()` has always defaulted to `all()` when no revset is given, so
113
+ `builtin_log()` is wired to the same set and can be composed like any other
114
+ revset function (`builtin_log() & mine()`).
115
+ - **`file.search({ ..., nameOnly: true })`** — jj v0.44 changed `jj file
116
+ search`'s default CLI output to print every matched *line* (prefixed with
117
+ its file path), moving the old "just the file paths" behavior behind a new
118
+ `--name-only` flag. isomorphic-jj's `file.search()` already returned
119
+ structured per-line matches (`{ path, lineNumber, line }`) — that was
120
+ already the new default — so the only gap was the old, path-only shape;
121
+ `{ nameOnly: true }` now returns a deduplicated `string[]` of matching
122
+ paths, matching `--name-only`.
123
+
124
+ ### Changed
125
+
126
+ - **`git_refs()` / `git_head()` revsets are now documented as deprecated.**
127
+ Upstream jj deprecated both in favor of `bookmarks()`/`tags()`/`@`, then
128
+ removed them outright in jj v0.43 (calling them in real jj now errors).
129
+ isomorphic-jj keeps them working — removing a function outright is a
130
+ breaking API change this pass didn't want to force on existing callers
131
+ unilaterally — but they're no longer listed in the revset engine's
132
+ "did you mean" suggestion text, and their JSDoc now calls out the upstream
133
+ removal and recommends `bookmarks()`/`tags()`/`@` instead. See "Not
134
+ changed" below for the case for a harder break in a future major version.
135
+
136
+ ### Testing
137
+
138
+ 1714 tests passing (was 1701); lint (0 errors), format:check, typecheck, and
139
+ build all still green.
140
+
141
+ ### Not changed (needs a human design call)
142
+
143
+ jj v0.43–v0.44 included several changes this pass deliberately left alone:
144
+
145
+ - **`jj run`** (v0.43 new command, gained `--passthrough`/`--ignore-changes`/
146
+ `--ignore-errors`/oldest-first-ordering in v0.44) — runs an arbitrary
147
+ subprocess against each revision's own private working copy. isomorphic-jj
148
+ doesn't shell out to anything and has to keep working in browsers with no
149
+ process/filesystem access; "run a command" doesn't have an isomorphic
150
+ equivalent. Adding it would mean either a Node-only stub (inconsistent with
151
+ the rest of the API surface) or scope-creeping into a job-runner. Flagging
152
+ for a maintainer decision rather than guessing.
153
+ - **Auto-importing remote tags/bookmarks during `git.fetch()`.** jj v0.44's
154
+ headline change is that `jj git fetch` now fetches tags the same way it
155
+ fetches bookmarks — as `<name>@<remote>`, auto-tracked by default. Wiring
156
+ that up properly for tags would need `git.fetch()` to actually populate
157
+ `TagStore`'s (and, by the same logic, `BookmarkStore`'s) remote-tracking
158
+ state from the fetched refs. Investigating this surfaced a **pre-existing**
159
+ gap: `git.fetch()` already doesn't call `bookmarks.setRemote()` for fetched
160
+ bookmarks today, even though `BookmarkStore.setRemote()`/`getRemote()` have
161
+ existed since v0.4 — remote ref sync was never wired all the way through at
162
+ the fetch call site. Building tag-fetch-tracking on top of that gap would
163
+ mean either (a) fixing the older bookmark gap too as a drive-by, our
164
+ papering over an inconsistency where tags "worked" and bookmarks didn't, or
165
+ (b) building a second, tag-only special case. Both are judgment calls
166
+ about a real architectural gap, not a mechanical port of one version's
167
+ changelog — left for a maintainer to decide. `tag.track()`/`untrack()`
168
+ (added this pass) at least let callers record and query tracking intent by
169
+ hand in the meantime.
170
+ - **`jj git clone --tag=PATTERN` / `--fetch-tags` removal.** Real jj replaced
171
+ `--fetch-tags=all|none|included` with `--tag=PATTERN` in v0.44. isojj's
172
+ `git.clone()`/`git.fetch()` never exposed CLI-shaped `--fetch-tags` flags in
173
+ the first place (just a boolean `noTags`), so there's no removed flag to
174
+ mirror; a `PATTERN`-based tag filter for clone/fetch would be new surface
175
+ area tied to the fetch-tracking gap above.
176
+ - **`jj git push --allow-conflicts`.** Real jj normally refuses to push
177
+ commits containing conflicts unless this flag is passed. isojj's
178
+ `git.push()` doesn't currently check for conflicts before pushing at all —
179
+ there's no existing guard to add a bypass flag for. Adding the guard itself
180
+ (and then the bypass) is more surface area than a one-version parity pass.
181
+ - **CLI argument parsing "last occurrence wins" (v0.44).** Checked
182
+ `bin/isojj.js`'s `parseArgs()`: it already stores each `--flag` into a
183
+ plain object keyed by flag name, so repeating a flag already naturally
184
+ overwrites the earlier value with no error — this was already jj v0.44's
185
+ new behavior by construction. No change needed.
186
+ - **`try(expr, fallback...)` template function, `jj workspace list` root
187
+ display, `diff.stat.max-bar-width`, `colors.crossed-out`, Gerrit/config-gc/
188
+ `/etc/jj` config-discovery changes.** These are jj's template engine and
189
+ terminal/config-file layers, which isomorphic-jj's CLI (a thin, generic
190
+ API-argument passthrough — see `bin/isojj.js`) doesn't attempt to replicate
191
+ 1:1 for any command. Out of scope for this pass; noted here for
192
+ completeness of the v0.43/v0.44 diff.
193
+
194
+ ## 0.0.0
195
+
196
+ - **Renamed: `isomorphic-jj` is now `@johnhenry/isomorphic-jj`, restarted at 0.0.0.** Same library, same API — a new address and era, not a maturity signal (1.7.0 lineage).
197
+
198
+ ```sh
199
+ npm install @johnhenry/isomorphic-jj
200
+ ```
201
+
202
+
203
+ All notable changes to isomorphic-jj are documented here. The project tracks the
204
+ [Jujutsu (jj)](https://github.com/jj-vcs/jj) CLI; each release notes the upstream
205
+ jj version whose semantics it targets.
206
+
207
+ ## [1.7.0] — Real revset parser, testable/UX-improved CLI, background-ops fix
208
+
209
+ Closes out the remaining known-issue backlog.
210
+
211
+ ### Changed — revset engine rewritten around a real tokenizer + parser + AST
212
+
213
+ `RevsetEngine.evaluate()` previously matched the whole trimmed expression
214
+ against one long chain of regexes, one per construct. This had two real bugs:
215
+
216
+ - **Nested function-call arguments silently mis-parsed.** A non-greedy regex
217
+ like `/^roots\((.+?)\)$/` truncates at the *first* `)` it sees, so
218
+ `roots(ancestors(x))` never worked — the previous "fix" for this was a
219
+ one-off hand-rolled paren-counter used only by `reachable()`.
220
+ - **No real operator precedence.** Mixed `&`/`|`/`~` expressions were
221
+ resolved by checking `.includes(' & ')` before `' | '` before `' ~ '`
222
+ regardless of the expression's actual structure.
223
+
224
+ The expression is now tokenized and parsed into a small AST via a real
225
+ recursive-descent / precedence-climbing parser (`&`/`~` at one precedence
226
+ tier, `|` lower, both left-associative), which is then evaluated by walking
227
+ the tree. This is purely a parsing-layer replacement — every per-function
228
+ filter/traversal method (`filterByAuthor`, `getAncestors`, `findForkPoint`,
229
+ etc.) is untouched, and **all 183 pre-existing revset tests pass unchanged**.
230
+
231
+ New capabilities (all strictly additive — nothing that worked before changed
232
+ behavior):
233
+ - Function-call arguments can nest arbitrarily deep: `roots(ancestors(x))`,
234
+ `heads(roots(ancestors(x)))`, etc.
235
+ - Parenthesized grouping: `(a | b) & c`.
236
+ - Quote-aware argument splitting: `description("a, b, and c")` no longer
237
+ mis-splits on the comma inside the quoted string.
238
+ - Whitespace around `&`/`|`/`~` is now optional (`a|b` works, not just `a | b`).
239
+
240
+ ### Fixed — background-ops (td-488842)
241
+
242
+ `enableAutoSnapshot()`'s timer callback awaited only `queue()`'s immediate
243
+ `{ id, promise }` handle, not the operation's own settling `promise`. Since
244
+ `queue()` resolves as soon as the operation is *enqueued* (not once it
245
+ finishes), a later `describe()` rejection landed on the unobserved promise
246
+ and became an unhandled promise rejection instead of reaching the
247
+ surrounding `catch`. Now awaits the returned `promise` too.
248
+
249
+ ### Changed — CLI (`bin/isojj.js`) rewritten to be testable, with real bugs fixed
250
+
251
+ The CLI had zero tests (calling `process.exit()` at import time made it
252
+ untestable) and several real bugs:
253
+ - **Made testable**: `parseArgs`/`formatOutput`/`findRepoRoot`/
254
+ `loadGitBackend`/`run` are now exported; `run()` returns an exit code
255
+ instead of calling `process.exit()` directly, guarded behind an
256
+ is-this-the-entrypoint check.
257
+ - **Fixed a parsing bug**: boolean flags (`dryRun`, `json`, `force`,
258
+ `interactive`, etc.) used to swallow the next token as a fake "value",
259
+ silently dropping a following positional — `describe --dryRun "fix typo"`
260
+ lost the commit message. Boolean flags never consume the next token now;
261
+ `--flag=true`/`--flag=false` is coerced to a real boolean.
262
+ - **Fixed a latent bug**: the CLI's own error message promised searching
263
+ "any parent up to mount point /" for a `.jj` repo but never actually did
264
+ so. Added `findRepoRoot()` to walk up parent directories like git/jj, so
265
+ the CLI now works from any subdirectory of a repo.
266
+ - **Wired up the real Git backend**: the CLI never passed `git`/`http` to
267
+ `createJJ()`, so `isojj init` silently ran in storage-only "mock" mode
268
+ with no real `.git` directory. It now dynamically imports isomorphic-git
269
+ (an optional peer dependency) when present, falling back gracefully when
270
+ it isn't.
271
+ - Added a full test suite (`tests/integration/cli.test.js`) and included
272
+ `bin/**/*.js` in coverage collection.
273
+
274
+ ### Testing
275
+
276
+ 1701 tests passing (was 1671); lint, format:check, typecheck, coverage
277
+ (91.2% branches), and build all green.
278
+
279
+ ## [1.6.1] — Bugfix batch
280
+
281
+ Fixes nine real, pre-existing defects that the v1.5/v1.6 coverage push
282
+ surfaced and documented (each had a test asserting the buggy behavior with a
283
+ `// BUG:` note — those tests now assert the corrected behavior instead).
284
+
285
+ ### Fixed
286
+
287
+ - **`bookmarks([pattern])` revset returned `[undefined]`.** `filterBookmarks()`
288
+ read `bookmark.target`, but `BookmarkStore.list()` returns objects keyed
289
+ `changeId`. It now reads `bookmark.changeId`.
290
+ - **`show()` never attributed bookmarks to a change**, for the same
291
+ `.target` vs. `.changeId` mismatch. Fixed the same way.
292
+ - **`bookmark.delete()`'s not-found guard was dead code.** It called
293
+ `bookmarks.get(name)` without `await`, so the "truthy Promise" always
294
+ passed the guard; the store's own `BOOKMARK_NOT_FOUND` was reached
295
+ instead of the friendlier method-level `NOT_FOUND`. Added the missing
296
+ `await`.
297
+ - **`operations.revert()` on a bookmark-move operation always threw
298
+ `BOOKMARK_EXISTS`.** Its "moved bookmark" branch called `bookmarks.set()`
299
+ (create-only) to move the bookmark back, but the bookmark still exists
300
+ during a move-revert. Changed to `bookmarks.move()`.
301
+ - **`ChangeGraph.getAncestors()` returned duplicate ancestors on
302
+ diamond-shaped history** (e.g. `a<-b`, `a<-c`, `b,c<-d`). A node was only
303
+ marked visited when dequeued, so a shared ancestor reachable via two
304
+ parents could be enqueued twice. Nodes are now marked visited the moment
305
+ they're enqueued.
306
+ - **`matchesPattern()` (merge driver registry) broke `**` glob patterns.**
307
+ Literal dots were escaped *after* `**` had already been converted to
308
+ `.*`, corrupting it into `\.*` (zero-or-more literal dots) instead of "any
309
+ characters". Dots are now escaped before the glob substitutions.
310
+ - **`IsomorphicGitBackend.getCurrentTree()` always threw
311
+ `TREE_READ_FAILED`.** It called `git.writeTree({ fs, dir })` without the
312
+ library's required `tree` argument (`git.writeTree` writes a single,
313
+ already-built tree — it doesn't build one from the working directory).
314
+ Reimplemented to walk the git index (`STAGE`) and build the nested tree
315
+ bottom-up, verified to produce the same oid as an equivalent real commit.
316
+ - **`ConflictModel._tryMergeDriver()`'s "no custom driver" guard never
317
+ matched.** It compared `driver === this.mergeDriverRegistry.defaultDriver`,
318
+ but `MergeDriverRegistry` never set a `defaultDriver` property (always
319
+ `undefined`), so every file was routed through the generic three-way
320
+ merge driver instead of falling back to the richer path-based conflict-type
321
+ detection. `MergeDriverRegistry` now exposes `defaultDriver`.
322
+ - **`IsomorphicGitBackend.stageAll()` double-wrapped per-file errors.** A
323
+ specific `STAGE_FILE_FAILED` thrown for one file was immediately re-wrapped
324
+ by the surrounding `catch` as the generic `STAGE_FAILED`, hiding the more
325
+ useful code. The outer catch now re-throws an already-categorized
326
+ `JJError` as-is.
327
+
328
+ ## [1.6.0] — Automatic working-copy snapshotting
329
+
330
+ Closes a long-standing fidelity gap: the library now **walks the working
331
+ directory on disk** and reconciles tracked state before read/commit operations,
332
+ so files created, modified, or deleted **out-of-band** (an editor, the shell,
333
+ `git checkout`) are picked up — matching jj's "snapshot before every command".
334
+ Previously only files written through `jj.write()` were ever tracked, so
335
+ `status`/`describe`/`diff`/`file.search`/`read` couldn't see anything else.
336
+
337
+ ### Added
338
+
339
+ - `WorkingCopy.walk()` — recursively lists working-directory files (excluding
340
+ `.git`, `.jj`, `node_modules`), robust across Node fs and in-memory fses.
341
+ - `WorkingCopy.snapshot()` — reconciles tracked file state with disk, returning
342
+ `{ added, modified, deleted }`; honors sparse patterns.
343
+ - `jj.snapshot()` — public method to trigger a snapshot explicitly.
344
+ - `createJJ({ autoSnapshot })` option (default `true`) to opt out of the
345
+ automatic behavior.
346
+
347
+ ### Changed
348
+
349
+ - `describe()`, `status()`, `diff()`, `read()`, `file.list()`, and
350
+ `file.search()` now auto-snapshot the working copy first (when operating on
351
+ `@`), so they reflect on-disk reality. `status()` now returns real `added` and
352
+ `removed` lists (previously always empty).
353
+ - A tracked file deleted on disk is now gracefully untracked on the next
354
+ snapshot instead of making `describe()` throw `SNAPSHOT_FILE_FAILED` (that
355
+ path still applies under `{ autoSnapshot: false }`).
356
+
357
+ ## [1.5.0] — Parity refresh (tracks jj through v0.43)
358
+
359
+ This release brings isomorphic-jj up to date with Jujutsu releases v0.31–v0.43,
360
+ adds a batch of revset functions and commands, and cleans up the toolchain.
361
+
362
+ ### Added — revset functions (jj v0.31–v0.43)
363
+
364
+ - `change_id(prefix)` / `commit_id(prefix)` — resolve a change by a hex prefix (jj v0.31).
365
+ - `subject(pattern)` — match only the first line of a description (jj v0.26).
366
+ - `author_name(x)`, `author_email(x)`, `committer(x)`, `committer_name(x)`,
367
+ `committer_email(x)` — fine-grained signature filters (jj v0.26).
368
+ - `signed()` — cryptographically signed changes (jj v0.29).
369
+ - `divergent()` — changes flagged as divergent (jj v0.38).
370
+ - `merges()` — canonical alias of the existing `merge()`.
371
+ - `forks()` — changes with more than one child.
372
+ - `first_parent(x)` / `first_ancestors(x)` — first-parent navigation (jj v0.32).
373
+ - `fork_point(x)` — youngest common ancestor of a set (jj v0.32).
374
+ - `merge_point(x)` — youngest common descendant of a set (implemented ahead of
375
+ upstream; jj stabilized its own `merge_point()` in v0.44 with matching
376
+ semantics — "the point where multiple branches merge").
377
+ - `exactly(x, n)` — the set, but errors unless it has exactly `n` elements (jj v0.34).
378
+ - `present(x)` — evaluate without erroring on unknown symbols.
379
+ - `coalesce(a, b, …)` — the first argument that resolves to a non-empty set.
380
+ - `remote_tags([pattern])` — remote (slash-qualified) tag targets (jj v0.38).
381
+ - `ancestors(x, depth)` — generalized to accept any nested revset plus an
382
+ optional depth limit (previously accepted only a bare 32-hex change ID).
383
+
384
+ ### Added — commands
385
+
386
+ - `revert({ revision })` — canonical replacement for `backout()` (jj renamed
387
+ `backout` → `revert` and removed `backout` in v0.35). `backout()` remains as a
388
+ deprecated alias.
389
+ - `redo()` — progressively re-applies operations reverted by `undo()` (jj v0.33).
390
+ - `sign()` / `unsign()` — record and clear signature metadata on a change (jj
391
+ v0.27). Previously hard `UNSUPPORTED_OPERATION` stubs; now implemented as
392
+ metadata operations that light up the `signed()` revset. (A pure-JS library
393
+ cannot verify GPG/SSH keys, so cryptographic verification is still delegated to
394
+ the Git layer.)
395
+ - `file.search({ pattern })` — search tracked file contents, regex by default
396
+ with `{ kind: 'substring' }` for literal matches (jj v0.37 / v0.41).
397
+ - `bookmark.advance({ name, to })` — move a bookmark forward only; refuses
398
+ non-descendant targets (jj v0.39).
399
+ - `tag.set({ name, changeId })` — create-or-move (upsert) a tag (jj v0.35).
400
+
401
+ ### Fixed
402
+
403
+ - **`tags()` revset now works.** It was a stub returning `[]` even though a
404
+ `TagStore` existed; the `RevsetEngine` is now wired to the tag store and
405
+ `tags()` / `tags(pattern)` return real results.
406
+ - **Duplicate `conflicts` key in the API object.** The raw `ConflictModel` was
407
+ exposed under the same key as the public `conflicts` namespace and silently
408
+ shadowed. The raw model is now available as `jj.conflictModel`.
409
+ - **`npm run lint` was completely broken** — the ESLint config was authored as an
410
+ ES module in a `"type": "module"` package. Renamed to `.eslintrc.cjs` and
411
+ fixed; the source tree now lints clean (0 errors).
412
+ - Latent `fail()` calls in tests (removed from jest-circus) replaced with `throw`.
413
+ - Removed an unnecessary try/catch, an empty catch block, an unused import, and
414
+ several unused variables flagged by the newly-working linter.
415
+
416
+ ### Changed
417
+
418
+ - `tsconfig.json` now includes the DOM/WebWorker libs and Node types so the
419
+ type-check reflects the library's real isomorphic runtime.
420
+ - Expanded the `isojj` CLI: `--version`, a far more complete `help`, positional
421
+ argument handling for `log`/`describe`/`file.*`, and generic pass-through of
422
+ all `--flags` so every API method is reachable from the CLI.
423
+
424
+ ## [1.4.3] and earlier
425
+
426
+ See the git history and [ROADMAP.md](./ROADMAP.md) for the v0.1–v1.4 development
427
+ record (core model, history editing, Git backend, first-class conflicts, revset
428
+ query language, workspaces, browser support, events, and full v1.0 JJ CLI parity).