isomorphic-jj 1.12.0 → 1.13.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 CHANGED
@@ -1,5 +1,113 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.13.0 — 2026-10-05 — Write-path fixes: converge on a fresh instance, new files lost on edit(), rebase content
4
+
5
+ Fixed in [PR #52](https://github.com/johnhenry/isomorphic-jj/pull/52).
6
+
7
+ Three bugs found while wiring a consumer (JJHub's isomorphic jj engine)
8
+ to write through this library, constructing one fresh `createJJ()`
9
+ instance per call so no read is ever served from a stale cache. Two of
10
+ them change observable behavior, hence a minor bump.
11
+
12
+ ### Fixed
13
+
14
+ - **`converge()` threw `WORKING_COPY_NOT_LOADED` on a fresh instance.**
15
+ Both outcomes record the working copy in their operation, but
16
+ `converge()` loaded only the graph and user config — so it only worked
17
+ after some earlier call on the same instance had loaded the working
18
+ copy. It now loads it itself. Regression tests: "on a fresh instance"
19
+ in `tests/integration/converge.test.js`.
20
+ - **A new file `write()` created was deleted by the next `edit()` away.**
21
+ `write()` tracks a file without recording it in the working-copy
22
+ change (the next command commits it, as in jj), but `edit()` first
23
+ deleted every tracked file missing from the change being left, as
24
+ cross-branch "pollution" from an earlier checkout. Since issue #30 every
25
+ switch fully syncs the working directory to its target, so nothing
26
+ leaks that way any more; what the cleanup actually removed was every new
27
+ file — top-level or nested — written into that change. Edits to files
28
+ already recorded survived, which made it look path-dependent. `edit()`
29
+ now commits every tracked file into the change it leaves, like
30
+ `new()`/`describe()`. **Behavior change:** a tracked file on disk is
31
+ now always the checked-out change's content when `edit()` leaves it;
32
+ code that removed a file from a change by editing the graph alone (not
33
+ `remove()`) must now remove it from disk too —
34
+ `tests/integration/issue-43-restore-reliability.test.js`'s
35
+ descendant-deletion test did exactly that and now uses `remove()`.
36
+ Regression tests: "write() then edit() away" in
37
+ `tests/integration/file-operations.test.js`.
38
+ - **`moveChange()`/`rebase()` kept the change's own tree verbatim.** The
39
+ three-way conflict check from issue #31 ran, but the rebased content
40
+ was never derived from it: the change kept every file only its OLD
41
+ parent had and never picked up what only the NEW parent changed. Its
42
+ content is now jj's rebase, path by path — the new parent's value
43
+ wherever the change didn't touch a path relative to its old parent,
44
+ the change's own value wherever it did (a path both sides changed
45
+ differently is the conflict #31 already reports, with markers). When
46
+ the moved change is checked out, its files on disk are synced to the
47
+ result. **Behavior change:** a rebased change's `fileSnapshot` now
48
+ differs from before for any path its old and new parents disagree on.
49
+ Regression tests: "content of the rebased change" in
50
+ `tests/integration/rebase-conflict-detection.test.js`.
51
+
52
+ ### Not changed
53
+
54
+ - `moveChange()` still re-derives only the moved change's own content,
55
+ not that of its descendants (they follow it structurally, by parent
56
+ change id, but keep their trees) — see the README's status section.
57
+ - Conflict detection still compares whole files, not lines: two sides
58
+ changing different lines of one file is a conflict.
59
+
60
+ ### Testing
61
+
62
+ 1882 tests passing, 0 skipped; lint, format:check, typecheck, and all 12
63
+ `examples/*.mjs` green; coverage 97.39% statements / 90.13% branches
64
+ (gate 90%). Each new regression test fails against 1.12.1's source.
65
+
66
+ ## 1.12.1 — 2026-09-27 — Conflict type shape + README status refresh
67
+
68
+ A cross-library documentation/typing audit flagged four things to verify
69
+ against current code; two turned out already fixed (`converge()`'s JSDoc
70
+ already documents N-way behavior as of #32/#35's 0.4.0 follow-up), one is
71
+ tracked as a design question instead of guessed at (see the "public,
72
+ untyped internal components" issue on `jj.graph` and its siblings), and
73
+ this release covers the two that were genuinely stale.
74
+
75
+ ### Fixed
76
+
77
+ - **`Conflict`/`ConflictType` in `src/types.d.ts` didn't match the shape
78
+ `ConflictModel` actually constructs.** `sides` was typed as `TreeRef[]`
79
+ (an array of tree hashes); every real `Conflict` object has `sides` as
80
+ an *object* — `{ left, right }`, `{ base, left, right }`, or (for
81
+ `converge()`'s N-way case, `ConflictModel.createNWayConflict()`)
82
+ `{ base, versions }` where `versions` is
83
+ `Array<{ commitId, content }>`. The interface was also missing
84
+ `message`, `timestamp`, and the optional `driverFailed`/`driverError`
85
+ fields that `_createConflict()` actually sets, and `ConflictType` was
86
+ missing the `'path'` and `'driver-conflict'` values `ConflictModel`
87
+ uses. Added a `ConflictSides` type covering both the two-way and N-way
88
+ shapes and corrected `Conflict` to match reality. Type-only change (the
89
+ `.d.ts` file has no runtime code), so nothing about the library's actual
90
+ behavior changes — this only fixes what TypeScript consumers see.
91
+ - **README's "Project Status" section was frozen at the pre-`@johnhenry`
92
+ rename numbering.** It read "Current Version: 0.3.0" (actual:
93
+ 1.12.0), cited "1823 tests" (actual: 1874), and its roadmap stopped at
94
+ 0.3.0 with no mention of 0.4.0's N-way `converge()` (#32/#35), the
95
+ 1.9.0 scoped/unscoped version-lineage merge (#36), or the 1.9.1–1.12.0
96
+ operation-log reliability pass (#37–#39, #41, #43, #45, #48). Also
97
+ corrected the provenance note's "reached v1.7.0 before the rename" to
98
+ the CHANGELOG's own recorded **v1.8.0** ceiling (see the 1.9.0 entry
99
+ below), and clarified that "Not yet 1.0" refers to API stability, not
100
+ the version number (which is already past 1.0 purely because merging
101
+ the two lineages forced it above the unscoped side's prior ceiling).
102
+ The Node 26 / `engines.node` note was checked too and was already
103
+ accurate — no change needed there.
104
+
105
+ ### Testing
106
+
107
+ 1874 tests passing, 0 skipped; lint (0 errors), format:check, typecheck,
108
+ and all 12 `examples/*.mjs` green; coverage 97.39% statements / 90.11%
109
+ branches / 99.08% functions / 97.63% lines (branch gate is 90%).
110
+
3
111
  ## 1.12.0 — 2026-09-27 — op-recording gaps left by #43/#45's fixes (#48)
4
112
 
5
113
  Follow-up to #43/#45 (below). Walking every op backward then forward now
package/README.md CHANGED
@@ -1020,10 +1020,10 @@ repo/
1020
1020
 
1021
1021
  ## Project Status
1022
1022
 
1023
- **Current Version**: 0.3.0 (`@johnhenry/isomorphic-jj`; same library/API lineage as the old `isomorphic-jj`, which reached v1.7.0 before the rename — see the provenance note at the top of this file)
1024
- **Test Coverage**: 1823 tests passing, ~97% statements / 91% branches
1023
+ **Current Version**: 1.13.0 (`@johnhenry/isomorphic-jj`; same library/API lineage as the old `isomorphic-jj`, which reached v1.8.0 before the rename — see the provenance note at the top of this file, and the CHANGELOG's `1.9.0` entry for how the scoped/unscoped version lineages were merged back together)
1024
+ **Test Coverage**: 1882 tests passing, ~97% statements / 90% branches
1025
1025
  **JJ CLI parity**: tracks Jujutsu through v0.45.1
1026
- **Status**: Actively developed; broad JJ CLI semantic parity, used in the examples/apps in this repo and in [JJHub](#family). Not yet 1.0 — API surface can still shift; pin a version and read the CHANGELOG before upgrading.
1026
+ **Status**: Actively developed; broad JJ CLI semantic parity, used in the examples/apps in this repo and in [JJHub](#family). Not yet 1.0 in the API-stability sense (the version number is past 1.0 only because merging the scoped/unscoped lineages forced it above the unscoped side's prior ceiling, not because the API is frozen) — API surface can still shift; pin a version and read the CHANGELOG before upgrading.
1027
1027
 
1028
1028
  **Completed:**
1029
1029
  - ✅ v0.1: Core JJ experience (stable IDs, undo, bookmarks, revsets)
@@ -1036,6 +1036,10 @@ repo/
1036
1036
  - ✅ 0.1.0 (post-rename): Parity refresh tracking jj through **v0.44** — `tag.track()`/`tag.untrack()`, the `builtin_log()` revset alias, and `file.search({ nameOnly: true })`; `git_refs()`/`git_head()` documented as deprecated (jj removed both in v0.43). See [CHANGELOG.md](./CHANGELOG.md) for what was deliberately left for a maintainer call (`jj run`, fetch-time remote tag/bookmark import).
1037
1037
  - ✅ 0.2.0: Storage-layer audit fixes — same-process storage locking, `undo()` reverting in-place graph/tag mutations, ambiguous change-id detection, categorized `push()` error codes, a conflict-marker newline fix, atomic protobuf writes, and open-ended revset ranges (`..b`, `a..`, `::`). See [CHANGELOG.md](./CHANGELOG.md).
1038
1038
  - ✅ 0.3.0: Parity refresh tracking jj through **v0.45.1** — a working `/browser` entry (async `createBrowserFS()`, bundle-safe protobuf schema loading, no stray Node-only imports), `undo()`/`operations.restore()` reverting graph rewrites with real progressive undo/redo, `squash()`/`abandon()`/`edit()`/`new()` fully syncing content instead of only metadata, `rebase()` performing a real three-way merge and recording conflicts, and the new `converge()` command (jj v0.45.0) to auto-resolve divergent commits. See [CHANGELOG.md](./CHANGELOG.md).
1039
+ - ✅ 0.4.0: `converge()` generalized from pairwise to N-way — any number of divergent copies of one change id resolve in a single call, matching how jj's own underlying engine already treats divergence internally (issue #32). See [CHANGELOG.md](./CHANGELOG.md).
1040
+ - ✅ 1.9.0: Scoped (`@johnhenry/isomorphic-jj`) and unscoped (`isomorphic-jj`) version lineages merged into one shared version going forward, with both names dual-published on every release.
1041
+ - ✅ 1.13.0: Write-path fixes — `converge()` works on a fresh instance, `edit()` no longer deletes new files `write()` created in the change it leaves, and `moveChange()`/`rebase()` derives the rebased change's content (new parent + the change's own edits) instead of keeping its tree verbatim. Not done: a rebase re-derives only the moved change's content, not its descendants', and conflicts are detected per whole file, not per line. See [CHANGELOG.md](./CHANGELOG.md).
1042
+ - ✅ 1.9.1 – 1.12.0: Operation-log reliability pass — `undo()`/`operations.restore()` correctness across write/snapshot/squash/rebase/abandon/edit, `log()` visibility fixes, and recording gaps in descendant rebases and conflict-resolution ops (issues #37–#39, #41, #43, #45, #48). See [CHANGELOG.md](./CHANGELOG.md) for the full per-fix breakdown.
1039
1043
 
1040
1044
  See [ROADMAP.md](./ROADMAP.md) for detailed plans, and [CHANGELOG.md](./CHANGELOG.md) for the release history.
1041
1045
 
package/dist/index.cjs CHANGED
@@ -9330,6 +9330,27 @@ async function createJJ(options) {
9330
9330
  await conflicts.addConflict(conflict);
9331
9331
  }
9332
9332
 
9333
+ // The rebased change's content, path by path: (new base) + (what the
9334
+ // change itself did relative to its old base), as jj's rebase. It
9335
+ // used to keep its own full tree verbatim — keeping every file only
9336
+ // the OLD base had and never picking up what only the NEW base
9337
+ // changed. A path both sides changed differently keeps the change's
9338
+ // own value here; if that's a real conflict, detectConflicts() above
9339
+ // reported it and the marker text below replaces it.
9340
+ const contentChanged = Boolean(oldParentChange);
9341
+ if (oldParentChange) {
9342
+ /** @type {Record<string, string>} */
9343
+ const rebased = {};
9344
+ const allPaths = new Set([...baseFiles.keys(), ...leftFiles.keys(), ...rightFiles.keys()]);
9345
+ for (const filePath of allPaths) {
9346
+ const baseValue = baseFiles.get(filePath);
9347
+ const ownValue = leftFiles.get(filePath);
9348
+ const value = ownValue === baseValue ? rightFiles.get(filePath) : ownValue;
9349
+ if (value !== undefined) rebased[filePath] = value;
9350
+ }
9351
+ change.fileSnapshot = rebased;
9352
+ }
9353
+
9333
9354
  // Materialize real conflict-marker text into the moved change's file
9334
9355
  // content — issue #43(3): rebase()/moveChange() used to record the
9335
9356
  // conflict correctly as DATA (via conflicts.addConflict() above) but
@@ -9371,6 +9392,13 @@ async function createJJ(options) {
9371
9392
  }
9372
9393
  }
9373
9394
 
9395
+ // A checked-out change's files on disk are its content: bring them in
9396
+ // line with the rebased snapshot (markers included), tracked, so the
9397
+ // next command's auto-snapshot finds nothing to re-record.
9398
+ if (contentChanged && changeId === workingCopy.getCurrentChangeId()) {
9399
+ await syncWorkingCopyFiles(change.fileSnapshot || {});
9400
+ }
9401
+
9374
9402
  // Update parent
9375
9403
  change.parents = [newParent];
9376
9404
  await graph.updateChange(change);
@@ -10354,29 +10382,18 @@ async function createJJ(options) {
10354
10382
  // see issue #29.
10355
10383
  const fileSnapshotBeforeSwitch = await snapshotFilesystem();
10356
10384
 
10357
- // CRITICAL FIX: Clean working directory BEFORE snapshotting to prevent cross-branch pollution
10358
- // Remove files that don't belong to the current changeId before we snapshot it
10385
+ // Commit the change being left: every tracked file on disk is its
10386
+ // content, as in new()/describe(). This used to first DELETE every
10387
+ // tracked file missing from the change's recorded fileSnapshot, as
10388
+ // "pollution" from an earlier checkout — but a switch now fully syncs
10389
+ // the working directory to its target (issue #30, below), so nothing
10390
+ // leaks that way any more, and what that cleanup actually deleted was
10391
+ // every new file write() had created in this change: write() tracks a
10392
+ // file without recording it in the change, so it is only committed
10393
+ // here, by the next command.
10359
10394
  if (previousChangeId !== args.changeId) {
10360
10395
  const previousChange = await graph.getChange(previousChangeId);
10361
10396
  if (previousChange) {
10362
- const previousSnapshot = previousChange.fileSnapshot || {};
10363
- const workingDirFiles = await workingCopy.listFiles();
10364
-
10365
- // Delete files that exist in working dir but NOT in previous changeId's snapshot
10366
- // This removes pollution from other branches that were previously checked out
10367
- for (const filePath of workingDirFiles) {
10368
- if (!previousSnapshot[filePath]) {
10369
- try {
10370
- const fullPath = join(dir, filePath);
10371
- await fs.promises.unlink(fullPath);
10372
- await workingCopy.untrackFile(filePath);
10373
- } catch (error) {
10374
- // Ignore errors - file might already be deleted or inaccessible
10375
- }
10376
- }
10377
- }
10378
-
10379
- // Now snapshot the CLEAN working directory (no pollution!)
10380
10397
  const currentSnapshot = await snapshotFilesystem();
10381
10398
  if (currentSnapshot && Object.keys(currentSnapshot).length > 0) {
10382
10399
  changeSnapshot[previousChangeId] = structuredClone(previousChange);
@@ -11681,6 +11698,9 @@ async function createJJ(options) {
11681
11698
  }
11682
11699
 
11683
11700
  await graph.load();
11701
+ // Both outcomes below record the working copy in their operation —
11702
+ // a fresh instance hasn't loaded it yet.
11703
+ await workingCopy.load();
11684
11704
  await userConfig.load();
11685
11705
  const siblings = graph.getDivergentSiblings(changeId);
11686
11706