Skip to content

Default to Subduction#34

Closed
expede wants to merge 8 commits into
mainfrom
sub-primary
Closed

Default to Subduction#34
expede wants to merge 8 commits into
mainfrom
sub-primary

Conversation

@expede

@expede expede commented Jun 9, 2026

Copy link
Copy Markdown
Member

Supersedes #22

I started this back in London 🇬🇧

TL;DR

The Main Show

  • Subduction is now the default sync backend
  • --sub is gone (use --legacy if you want the old relay)
  • Config is now versioned (v0 -> v1) and migrates in place, so existing repos keep working
  • To get this working I needed some tests, but several existing ones were broken

Bug Fixes

  • Two real bugs fixed: binary files were getting mangled through sync (a 256-byte file came back as 461 bytes of mojibake)
  • require("pushwork") would actually run the CLI against your process's argv
  • A pile of smaller CLI hardening (URL validation, conflicting-flag rejection, crash guards, saner error output)

"Other"

  • Set up GitHub CI
  • Added tooling the repo: ESLint, package validation, spell-check, bumped nix flake to 26.05
  • The test suite is mostly real now — I deleted the ones that were testing Node's stdlib or a hardcoded while loop and added coverage for the stuff that was actually untested

Example v1 Config

$ cat ../.pushwork/config.json
{
  "config_version": 1,
  "protocol": "subduction",
  "sync_enabled": true,
  "exclude_patterns": [
    ".git",
    "node_modules",
    "*.tmp",
    ".pushwork",
    ".DS_Store"
  ],
  "artifact_directories": [
    "dist"
  ],
  "sync": {
    "move_detection_threshold": 0.7
  },
  "sync_server": "wss://subduction.sync.inkandswitch.com"
}

expede added 8 commits June 9, 2026 17:45
Subduction was previously opt-in via `--sub`. Make it the default sync
backend and add `--legacy` to opt back into the classic WebSocket relay.
Backend choice is persisted per-directory as a `protocol` field in a new
versioned config; v0 configs are migrated to v1 in place (with a backup)
and their backend inferred, so existing repos keep working unchanged.

- `protocol: "subduction" | "legacy"` replaces the boolean `subduction`
  flag; `--sub` is removed.
- `config_version` + `resolveProtocol()` + `migrateIfNeeded()` drive the
  v0->v1 upgrade and infer the backend for pre-existing configs.
- `--sync-server`'s storage id is legacy-only; supplying one in the default
  Subduction mode is now a hard error rather than a silent no-op.
- Replace the `sub-flag` integration test with `legacy-flag`; add
  config-migration and subduction-config coverage.
`src/index.ts` re-exported `./cli`, whose module top-level ran
`program.parseAsync()`. Importing the package therefore parsed the host
process's argv -- printing help and exiting, or even running a pushwork
command. Guard the CLI bootstrap behind `require.main === module` and stop
re-exporting it, so `require("pushwork")` has no side effects.

Curate the public surface to the high-level API (SyncEngine, ConfigManager,
SnapshotManager, createRepo, ...) instead of dumping every internal util,
and add a regression test that the package imports cleanly.
A binary file with no NUL byte in its first 8 KB was classified as text and
read with `fs.readFile(path, "utf8")`, replacing invalid UTF-8 sequences
with U+FFFD -- a 256-byte file came back as 461 bytes of garbage on clone.

Classify content as text only when it is *losslessly* valid UTF-8 (a fatal
streaming decode), and guard `readFileContent` to return a string only if
the bytes survive a round-trip. Add a property-based round-trip test over
arbitrary byte buffers plus the null-free-binary repro.
`checkout` opened a networked Repo via setupCommandContext but never shut it
down or exited, so the open connection kept the process alive forever -- for
a command that only prints "not implemented". Resolve the path, print the
message, and exit non-zero without opening a repo. Hide the experimental
`checkout`/`log` stubs from `--help` so the CLI stops advertising
non-functional commands.
- `clone`/`track` only checked the `automerge:` prefix, so a typo'd URL
  produced a silent empty clone; validate the full document id with
  `isValidAutomergeUrl`.
- `sync --gentle --nuclear` silently ran nuclear; reject the combination.
- Render expected errors (e.g. "not initialized") as a clean one-line
  message and show the internal stack only under DEBUG.
- Lower Subduction's Rust log level so WARN transport chatter stops leaking
  to stdout, and filter the benign negative-timeout warning emitted by
  automerge-repo's throttle.
- `ConfigManager.load()` silently fell back to defaults on an unreadable
  config; warn instead so corruption isn't invisible.
- `url` crashed with a stack trace on a corrupt snapshot.json; tolerate it.
- `init <file>` failed deep inside with a raw ENOTDIR; reject a
  non-directory path up front.
- Fix README drift: drop the non-existent `--debug` flag, correct the `ls`
  flag / Node version / source tree, document `sync --gentle`/`--nuclear`.
Stand up the quality tooling the project lacked:

- ESLint (flat config) with typescript-eslint and type-aware promise rules;
  fix what it found (floating/misused promises in `watch`, unnecessary
  assertions). The old `lint` script used a removed flag with no config.
- GitHub Actions CI: build, typecheck, lint, package-validate, unit tests,
  plus a typos spell-check job.
- `publint`/`attw` (`check:package`), which surfaced that the library
  shipped no `types`/`files` -- fix package.json `exports` accordingly.
- `typos` config + Nix dev-shell entry; bump the flake to nixos-26.05 and
  drop the removed `nodePackages` attribute.
Several "tests" exercised Node's fs/timers or hand-written simulation loops
rather than pushwork code: `sync-timing` (pure stdlib), `sync-convergence`
(asserts on a hardcoded while-loop), and `deletion-behavior`/`sync-deletion`
(never invoke the engine -- they `new` ChangeDetector and discard it). Delete
them; the behavior is covered by `in-memory-sync` and `fuzzer`.

Add direct coverage for the previously-untested core logic: MoveDetector
(threshold, size guard, binary short-circuit, best-match) and text-diff's
`spliceText` (a round-trip property over our cursor arithmetic, incl.
unicode). Fix `sync-flow`'s corrupt-config test, which wrote snapshot.json
but asserted on config.load(), passing for the wrong reason.
@expede expede mentioned this pull request Jun 12, 2026
@expede expede marked this pull request as ready for review June 12, 2026 21:00
@expede expede closed this Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant