Releases: daniloaguiarbr/atomwrite
atomwrite v0.1.20 — 11 GAP-2026 closed, --lang → --locale rename, 542 tests, 4 ADRs
Highlights
- 11 GAP-2026 closed (001-011) — see
gaps.mdfor the full local audit - 542 tests in 47 suites (+27 from v0.1.19's 515 in 46 suites: 11 GAP-2026 closures + 16 intention-guard tests)
- 4 ADRs added: ADR-0034, ADR-0035, ADR-0036, ADR-0037
- BREAKING CHANGE in CLI surface: global
--langrenamed to--locale- Env var
ATOMWRITE_LANGstays stable - Rust field
args.global.langstays stable - Subcommand
--lang(e.g.scope --lang rust,transform --lang python) now works as a freed alias for--language
- Env var
GAP-2026 Closures (11/11)
| GAP | Subcommand | Type | ADR |
|---|---|---|---|
| 001 | count --by-size |
help-first drift | — |
| 002 | write --preserve-timestamps |
feature parity | — |
| 003 | scope --lang alias |
help-first + namespace | 0037 |
| 004 | write --line-ending crlf |
form-accept (crlf + cr-lf) | — |
| 005b | edit --partial semantics |
single vs multi-pair asymmetry | 0036 |
| 006 | diff --algorithm regression |
pinning | — |
| 007 | count --by-extension backup |
parsing edge case | — |
| 008 | read --head/--line/--lines |
API contract ambiguity | — |
| 009 | read mode discriminator |
API metadata | — |
| 010 | search --no-begin-end |
output noise | — |
| 011 | write intention guards |
defense-in-depth | 0035 |
GAP-2026-011: Write Intention Guards (CRITICAL)
On 2026-06-15, atomwrite write was called without --append on c24-framework34.html (491,827 bytes). The file was truncated to a few bytes; ~127 lines of work (~9 KB) were lost. A 2026-06-14 23:49 manual cp backup existed but did not cover 2026-06-15 work.
v0.1.20 adds 5 layers of defense-in-depth (all opt-in, default off to preserve backward compat):
--require-backup(L2): aborts withInvalidInput(exit 65) if target exists and--backupis not set--confirm(L3): promptsOverwrite <path> (<bytes> bytes)? [y/N]for files larger than 100 KB--auto-rotate(L5): forces a rotation backup if target was modified within the last 24 hours--risk-threshold <PERCENT>(default 50): L1 size-guard threshold; emits stderr warning (low/medium/high) when size delta exceeds the thresholdWriteOutput.risk_assessmentfield (L6 telemetry): carries original/new bytes, delta percentage, risk level, and which guard triggered
Migration
Breaking change in CLI surface: scripts using --lang <locale> must migrate to --locale <locale>.
# one-liner migration across bin/ and scripts/
rg -l '\-\-lang\b' bin/ scripts/ && sd -- '\-\-lang\b' '--locale' bin/ scripts/Env var ATOMWRITE_LANG and the programatic args.global.lang field remain stable — CI matrices, container wrappers, and Rust consumers do not need changes.
Validation
cargo build --release— OKcargo clippy --all-targets -- -D warnings— OK- 542 tests in 47 suites (up from 515 in 46 in v0.1.19; +27 new: 11 GAP-2026 closures + 16 intention-guard tests)
- 4 new ADRs: 0034 (help-driven testing anti-pattern), 0035 (write intention guards), 0036 (edit --partial coverage), 0037 (global locale rename)
- 11 GAP-2026 closed (001-011), 100% coverage of local audit gaps
- Cross-compile verified on 3 Windows targets:
x86_64-pc-windows-gnu,i686-pc-windows-gnu,x86_64-pc-windows-msvc
Resources
v0.1.19 — G121 path resolution, real S-expression, 7 exit code drifts
[0.1.19] - 2026-06-14
G121 — search and replace resolve root paths against the workspace
- Bug (CWE-367 — TOCTOU/path-confusion) —
cmd_searchandcmd_replacetook the caller-supplied root paths, validated them withpath_safety::validate_path, then fed the ORIGINAL (CWD-relative) path toignore::WalkBuilder.validate_pathreturned the canonical absolute path but the per-call result was discarded. WhenCWD != --workspace, the walker either (a) silently walked the wrong tree if a same-named path existed under CWD, or (b) produced aJailViolationevent per file walked. The G118 fix inwrite.rs:44(ADR-0027) never propagated to these two commands because the per-entryvalidate_pathinside the worker thread masked the missing pre-step resolution. - Fix — new
path_resolution::resolve_paths_against_workspacehelper —cmd_searchandcmd_replacenow call this helper once at the top of the command (afterglobal.resolve_workspace()()) and pass the canonicalVec<PathBuf>tobuild_walker. The pre-stepfor path in &args.paths { validate_path(path, &workspace)?; }loop inreplace.rsis removed; search gets a parallel resolution call. Bothbuild_walkersignatures gain acanonical_paths: &[PathBuf]parameter; they no longer read&args.paths[0]directly. - Consequence — Search and replace now honor
CWD != --workspace. A relative path likesrc/passed with--workspace /path/to/wswalks/path/to/ws/src/regardless of process CWD. Out-of-jail paths fail once withWORKSPACE_JAIL(exit 126) at command start instead of per-file inside the worker. Seedocs/decisions/0031-g121-path-resolution-helper.md.
G122 — Real S-expression matching in query subcommand
- Bug (silent feature, documented but never implemented) — The
querysubcommand (v14 Tier 3, introduced in v0.1.12) always promised S-expression support. The docs inCOOKBOOK.md,HOW_TO_USE.mdand both bilingual SKILL files show examples likeatomwrite --workspace . query src/main.rs --query "(function_item name: (identifier) @name)". In practice,cmd_querycalledwalk_kind_filterwhich doeswanted.iter().any(|w| w == &kind)— a literal STRING comparison withnode.kind(). The whole string"(function_item name: (identifier) @name)"never matched any realnode.kind(), so the S-expression feature never worked. - Fix — new
QueryTypeenum + auto-classification —classify_pattern(pattern) -> QueryTypedetects S-expression by the presence of(,), or@.walk_sexprcompiles the pattern viatree_sitter::Query::new, executes viaQueryCursor::matches, and emitsquery_matchNDJSON with the newcapture_namefield for each@capture.cmd_querybranches on the classification. - New direct dep
tree-sitter = "0.26"— the language-pack re-exportsLanguagebutQuery/QueryCursor/StreamingIteratorrequire thetree-sittercrate itself. - Consequence — Patterns with
(,), or@are routed totree_sitter::Query::new. Parse errors (e.g.(unclosed) return exit 1 withinvalid S-expression pattern: ...message (viaanyhow::Context). The kind-filter path is preserved bit-by-bit: users passing--query function_item(without S-expression chars) keep getting the same results. Seedocs/decisions/0032-query-sexp-real-implementation.md.
Exit code documentation drift consolidation (ADR-0033)
- Context — Phase D testing on 2026-06-14 ran 7 concrete binary-level probes against the v0.1.18 release and surfaced 7 places where the published docs diverged from the actual binary behavior. The 7 drifts are:
STATE_DRIFT(82) absorbsCHECKSUM_VERIFY_FAILED(81) for--verify-checksum— both are conflict class, retryable. The 81-code is now historical (preserved only for thereadpath BLAKE3 mismatch on file content).--syntax-checkreturnsSYNTAX_ERROR_DETECTED, NOTSYNTAX_ERROR— the rename happened in the v0.1.12 G72 tree-sitter rollout but docs were not updated.ORPHAN_JOURNAL(93) is consultive, NOT auto-detected — the gate isATOMWRITE_WAL=1OR--strict-atomic. The defaultwrite(v0.1.16 G119WalPolicy::Auto) does not write a sidecar and therefore cannot detect orphans.BROKEN_PIPE(141) requires real SIGPIPE propagation — a simplehead -1pipe does NOT trigger it. The v0.1.4+ SIGPIPE restoration puts the default disposition back, so the signal is only raised when the downstream consumer actively closes the pipe mid-stream.- Binary file reads return exit 0 with
kind: binarymetadata, NOT exit 65 — the v0.1.4BINARY_FILEheuristic was changed to emit a structured envelope and exit 0. The 65-code path now only fires forreadwithout--format rawAND with the binary heuristic bypassed. - Missing positional argument returns
ARGUMENT_PARSE_ERROR(exit 2), NOTINVALID_INPUT(65) — clap-level argument errors are reported as exit 2. The 65-code is reserved for runtime content validation (malformed TOML, invalid regex, empty stdin default). - Missing
--workspacedefaults to CWD, NOT an error —--workspaceis documented as a flag with a CWD default, not a required argument.WORKSPACE_JAIL(126) only fires when an absolute path resolves outside the effective jail.
- Decision — Accept the binary behavior as canonical. Consolidate the docs in v0.1.19 to match. See
docs/decisions/0033-v0-1-19-exit-code-naming-drift-consolidation.md. - Note on
SYNTAX_ERRORlegacy name — the v0.1.12 docs usedSYNTAX_ERROR; the binary in v0.1.18 emitsSYNTAX_ERROR_DETECTED. The historical name is preserved only in prose for grep-ability.
Validation
cargo build --releaseOKcargo clippy --all-targets -- -D warningsOK- 515 tests passing in 46 suites (up from 502 in 44 suites in v0.1.18, +13 new)
- 3 new ADRs: 0031 (path resolution helper), 0032 (query S-expression), 0033 (exit code drift consolidation)
[0.1.18] - 2026-06-14
v0.1.18 — G117, G118+R, G119, G120, 502 tests
[0.1.18] - 2026-06-14
G118 — replace pre-validates root paths against the workspace jail
cmd_replaceresolve-first para todas as raízes — apósglobal.resolve_workspace(), o comando itera sobreargs.pathse chamapath_safety::validate_path(path, &workspace)?para CADA raiz ANTES de construir oWalkBuilder. Falha rápida comWORKSPACE_JAIL(exit 126) na primeira violação. Comportamento legado per-entry (v0.1.12-v0.1.17) emitia um eventoJailViolationpor arquivo caminhado e o usuário via o diagnóstico enterrado sob N eventos. Agorareplace /etc/passwdaborta em microssegundos com um único envelope de erro estruturado.- Convenção resolve-first agora universal —
write(ADR-0027),edit,copy,apply,move,rollback,set,del,casee agorareplacetodos validam o alvo contra o jail workspace ANTES de qualquerexists()ou read. Um único modelo mental para todos os comandos mutantes. - Teste de regressão atualizado —
replace_jail_violation_does_not_inflate_counteremtests/cli_v012_regressions.rsagora afirma exit 126 + envelopeWORKSPACE_JAIL+ arquivos inside/outside inalterados. O nome é preservado (a invariante subjacente — "jail violation não pode inflar o counter de substituições" — é mantida) mas o corpo da asserção mudou. - 2 novos testes integrados em
tests/cli_replace.rs:replace_root_path_outside_workspace_exits_126(caminho absoluto/etc/passwd) ereplace_relative_dotdot_root_outside_workspace_exits_126(caminho relativo../escape).
G120 L3 — cobertura de teste para cross-validação
- 2 novos testes integrados em
tests/cli_write.rs:g120_l3_append_empty_stdin_with_matching_checksum_succeeds— escreve arquivo seed, hasheia, rodawrite --append --allow-empty-stdin --expect-checksum <HASH> < /dev/null, afirma exit 0 +stdin_bytes_read: 0+ arquivo inalterado (no-op append preserva checksum).g120_l3_append_empty_stdin_without_opt_in_rejects_at_l1— sem--allow-empty-stdin, a guarda L1 dispara primeiro (exit 65) e o arquivo é preservado. Documenta que L3 é inalcançável sem opt-in explícita.
G117 follow-up — cobertura de edge cases
- 3 novos testes integrados em
tests/cli_edit.rs:edit_unicode_old_new_exact_match— diacríticos UTF-8 (ção→AÇÃO) com casamento exato byte-a-byte. Documenta o contrato de single-pair (substitui apenas a PRIMEIRA ocorrência; multi-pair para múltiplas).edit_crlf_line_endings_preserve_eol_after_replace— input com\r\n, replace, afirma preservação byte-a-byte (sem colapso para\n).edit_multi_pair_same_old_appears_twice_applies_both— multi-par onde ambos os pares referenciam o mesmo token--old. Garante que o segundo par consome a versão pós-primeiro-substituição (proteção contra off-by-one).
ADR
docs/decisions/0030-v0-1-18-g118-replace-pre-validation-g120-l3-tests-g117-edge-cases.md— registra as 3 decisões, alternativas consideradas e gatilhos para revisitar.
Validation
cargo build --releaseOKcargo clippy --all-targets -- -D warningsOK- Suíte completa de testes: 502 testes passando, 0 falhas, 0 regressões introduzidas pelas 3 mudanças
- 2 flakes pré-existentes confirmados como não relacionados (
signal_test::batch_interrupted_by_signal,tracing_test::span_captures_path_field+tracing_test::debug_level_includes_filter_info) — falhas idênticas no baselinegit stashantes das mudanças
v0.1.14 — fix `write --line-ending auto` cross-platform divergence on new files
Summary
Resolves the write_creates_file_with_ndjson_output integration test failure on the windows-2025-vs2026 GitHub Actions runner. v0.1.13 produced different bytes_written values for the same stdin content on Linux/macOS (12 bytes) vs Windows (13 bytes) when the target file did not exist.
Root Cause
In src/commands/write.rs::normalize_line_endings, the Auto mode had a cfg!(windows) ? CrLf : Lf fallback for the new-file / unreadable-file branch. On Windows, the subsequent line_endings::normalize(..., CrLf) inserted a \r before every \n, inflating the byte count by 1. A secondary bug also stripped a byte from CRLF input on Linux/macOS via the same fallback path.
Fix
Both cfg!(windows) fallback branches have been removed. Auto on a new file is now a true no-op, matching the LineEnding::Auto docstring ("Preserve the dominant ending of the original file"). Behavior is deterministic across Linux, macOS, and Windows for the same stdin content. Detection on existing files and explicit --line-ending lf|cr-lf|cr modes are unchanged.
Regression Tests
Added in src/commands/write.rs::tests:
auto_on_new_file_preserves_lf_input- `auto_on_new_file_preserves_crlf_input"
Validation
cargo build --all-features(Linux): PASScargo fmt --check: PASScargo clippy --all-targets --all-features -- -D warnings: PASScargo test --all-features: 152 lib tests + integration suites + 3 doctests PASScargo check --target x86_64-pc-windows-gnu --libwithRUSTFLAGS=-Dwarnings: PASS, 0 errors, 0 warnings- 8/8
cli_writeintegration tests pass
Links
v0.1.13 — fix 4 RUSTFLAGS=-Dwarnings errors on windows-2025-vs2026
Summary
Resolves 4 compile errors on the windows-2025-vs2026 GitHub Actions runner under RUSTFLAGS=-Dwarnings. All four errors were unix-only symbols that become dead code when compiled for Windows; none were caught by the Linux CI matrix because Linux is cfg(unix).
Fixed
unused import: Durationinsrc/lock.rs:24—std::time::Durationis gated behind#[cfg(unix)]since it is only consumed by theflockpolling loop intry_acquire_loop.unused variable: strict_atomicinsrc/atomic.rs:381— annotated with#[cfg_attr(not(unix), allow(unused_variables))], mirroring the pattern already validated insrc/signal.rs:15-17.function copy_tempfile_to_target is never usedinsrc/atomic.rs:604— gated behind#[cfg(unix)]since the function is only invoked from the EXDEV fallback branch.clippy::unnecessary_literal_unwrapinsrc/atomic.rs:195—hardlink_nlink.unwrap_or(1) > 1rewritten ashardlink_nlink.is_some_and(|n| n > 1)to avoid triggering clippy 1.94+ on the WindowsNonepath.
Validation
cargo build --all-features(Linux): PASScargo clippy --all-targets --all-features -- -D warnings(Linux): PASScargo test --all-features: 150 lib tests + integration + doctests PASScargo check --target x86_64-pc-windows-gnu --libwithRUSTFLAGS=-Dwarnings: PASS, zero errors, zero warningscargo fmt --check: PASS
Links
atomwrite v0.1.12 — G72 Tree-sitter + G114 WAL + v14 Tier 3
Highlights
This release closes 20 technical gaps with 11,300+ lines of new code, tests, schemas, ADRs, and bilingual documentation.
Major Features
- G72 — REAL tree-sitter syntax check (
atomwrite write --syntax-check): validates content against 24 languages viatree_sitter_language_pack - G114 — WAL sidecar consultive: opt-in via
ATOMWRITE_WAL=1env or--strict-atomicflag, withrecover_orphan_journalsAPI - v14 Tier 3 — 6 new subcommands:
set,get,del,case,query,outline
6 New Subcommands (ADDITIVE)
| Subcommand | Description |
|---|---|
set |
Write a value to a TOML/JSON config file with dotted path |
get |
Read a value from a TOML/JSON config file with dotted path |
del |
Delete a key from a TOML/JSON config file |
case |
Convert identifier case (snake, camel, pascal, kebab, screaming-snake) |
query |
Tree-sitter S-expression query against a source file |
outline |
Extract high-level structure from a source file |
5 New Error Codes (ADDITIVE)
| Code | Variant | Class | Retryable |
|---|---|---|---|
| 83 | LockTimeout |
transient | Yes |
| 88 | SyntaxError |
permanent | No |
| 91 | ExdevFallbackDisabled |
precondition_failed | No |
| 92 | CopyBackBlake3Failed |
conflict | After re-read |
| 93 | OrphanJournal |
precondition_failed | No |
Quality Gates
- 445 tests in 44 test suites (was 320 in v0.1.10, +29 in v0.1.11, +96 in v0.1.12)
- 8 gates pass:
fmt,clippy,build,test,doc,deny,audit,msrv - 3 cross-compile targets:
x86_64-pc-windows-gnu,i686-pc-windows-gnu,x86_64-pc-windows-msvc - MSRV: Rust 1.88 stable
cargo deny: 4/4 OK (zero vulnerabilities)
New Modules
src/wal.rs— G114 WAL sidecar with consultive recoverysrc/syntax_check.rs— G72 tree-sitter syntax validationsrc/commands/{set,get,del,case,query,outline}.rs— v14 Tier 3 subcommandssrc/lock.rs— G54 file lock with timeoutsrc/xattr_restore.rs— G39 extended attribute preservation
Documentation
- 7 ADRs in
docs/decisions/(0019-0025) - 7 new JSON Schemas in
docs/schemas/ - 14 docs (7 EN + 7 PT-BR) updated with v0.1.12 sections
- Skill files (
skill/atomwrite-{en,pt}/SKILL.md) expanded from 35 to 46 sections
Dependencies
tree-sitter-language-pack = "1.8"(download + dynamic-loading, ~5-10MB footprint)
Breaking Changes
None. All 6 new subcommands and 5 new error variants are ADDITIVE. No existing subcommand was renamed or removed.
Installation
cargo install atomwrite --locked --version '^0.1.12'Verification
atomwrite --version
# atomwrite 0.1.12
atomwrite --help
# 28 user-facing subcommands
atomwrite read src/main.rs | jaq -r '.checksum'
# BLAKE3 checksumMigration from v0.1.11
No action required. The release is fully backward compatible. See CHANGELOG.md for the full list of changes.
Resources
- Crate: https://crates.io/crates/atomwrite
- Repository: https://github.com/daniloaguiarbr/atomwrite
- Documentation:
README.md,CHANGELOG.md,docs/ - ADRs:
docs/decisions/0019-0025.md - Schemas:
docs/schemas/
v0.1.11
atomwrite v0.1.11### Fixed- Windows E0433 on windows-2025-vs2026 — libc::write(STDERR_FILENO, ...) was referenced from src/main.rs in a function compiled on every platform, but libc is declared only under [target.cfg(unix).dependencies]. The shutdown-message writer was moved to src/signal.rs and gated with #[cfg(unix)].- signal_test::shutdown_message_on_stderr no longer flakes on ubuntu-latest — Two independent failure modes addressed: 1. cmd_search short-circuits to Ok(()) when shutdown.is_shutdown() is true, so the main thread takes the Ok(()) branch and emits the shutdown banner. 2. install_handlers_early and install_handlers now share a single Arc<ShutdownSignal> instance, eliminating the race where the second instance flag remained false under signal-hook chain-of-handlers ordering.- Test uses ATOMWRITE_READY_FILE for race-free readiness detection — atomwrite writes its PID to the file as soon as install_handlers_early returns; the test polls the file with a 10s deadline before sending SIGINT.### Validation- 302/302 tests pass across 33 test suites (5 successive full-suite runs, 0 failures)- cargo fmt, cargo clippy --all-features --all-targets -- -D warnings, cargo build --release, RUSTDOCFLAGS=-D warnings cargo doc --no-deps --all-features, cargo audit, cargo deny check all PASS
v0.1.10
Highlights
- CI GitHub Actions fully green — The persistent ubuntu-latest
signal_test::shutdown_message_on_stderrfailure has been fixed by installing signal handlers as the very first initialization step inmain(). The previous v0.1.8/v0.1.9 fixes (POSIX signal-safety, stderr flush) were correct in isolation but irrelevant: the child was being killed by SIGINT before any of that code could run. - GAP 20 resolved — Root cause: the SIGINT sent by
signal_testwas arriving BEFOREinstall_handlers()had a chance to register the signal_hook flag and low_level handlers. The default signal disposition (terminate via signal) was executing, killing the child with signal 2 and bypassing the entire graceful-shutdown path.
Fixed (GAP 20 - early signal handler installation)
-
signal_test::shutdown_message_on_stderrno longer fails on Linux CI — Addedsignal::install_handlers_early()which registers only the async-signal-safesignal_hook::flag::registerfor SIGINT and SIGTERM as the very first statement inmain(), before any other initialization. This guarantees that any SIGINT/SIGTERM arriving from the first nanosecond of process startup is caught and sets the shutdown flag, which the search/batch loops check. The fullinstall_handlers()(with counter tracking and OnceLock shared state) runs later and adds the double-Ctrl+C force-kill logic on top, without overwriting the early flag handlers. The previous v0.1.8/v0.1.9 fixes for POSIX signal-safety and stderr flush are still in place and necessary: the early install guarantees the flag is set, the main-thread writeln guarantees the message reaches the captured stderr pipe before exit. -
src/main.rsshutdown message useslibc::write(STDERR_FILENO, ...)directly — Replacedwriteln!(io::stderr().lock(), ...)with a helper function that callslibc::write(STDERR_FILENO, ...)to bypass any userspace buffering. The function is marked#[allow(unsafe_code)]becausemain.rshas#![deny(unsafe_code)]. The write is async-signal-safe per POSIX.1-2017 signal-safety(7) and goes straight to the kernel via fd 2, guaranteeing the bytes reach the captured stderr pipe before the process exits.
Validation
cargo build --all-features: PASScargo clippy --all-features --all-targets -- -D warnings: PASScargo test --all-features: 302/302 tests PASS (5/5signal_testcases all pass)cargo fmt -- --check: PASScargo audit: PASS (no vulnerabilities, no--ignoreflag)cargo deny check: PASS (advisories, bans, licenses, sources all OK)- 5/5 consecutive local runs of
signal_test::shutdown_message_on_stderrPASS
Notes
- v0.1.10 is a NON-BREAKING bug fix. No public API was modified.
- The signal handler installation is now a two-phase process: minimal flag handlers at process start, full counter handlers after init. The OnceLock ensures only one full install takes effect.
- See
CHANGELOG.mdfor full details.
v0.1.9
Highlights
- CI GitHub Actions fully green — Final flush fix for
signal_test::shutdown_message_on_stderrensures the shutdown message reaches the captured stderr pipe before the process exits on Linux/glibc. - POSIX signal-safety compliance — The shutdown message is emitted via
io::stderr().lock()in the main thread, with theStderrLockguard guaranteeing buffer flush on Drop.
Fixed (GAP 17 follow-up)
signal_test::shutdown_message_on_stderrflushes viaio::stderr().lock()— The first v0.1.8 fix movedeprintln!from the SIGINT/SIGTERM signal handlers to the main thread, but usedwriteln!(io::stderr(), ...)which is fully-buffered when stderr is redirected to a pipe (as incargo test'sStdio::piped()). The buffer was never flushed before the process exited with the signal exit code, so the parent test saw an empty stderr. The fix usesio::stderr().lock()to acquire theStderrLockguard, which flushes the buffer on Drop. CI ubuntu-latest will confirm on push.
Validation
cargo build --all-features: PASScargo clippy --all-features --all-targets -- -D warnings: PASScargo test --all-features: 302/302 tests PASS (5/5 runs ofsignal_test::shutdown_message_on_stderrPASS)cargo fmt -- --check: PASScargo audit: PASScargo deny check: PASS
Notes
- v0.1.9 is a NON-BREAKING patch over v0.1.8. The only code change is
writeln!(io::stderr(), ...)→writeln!(io::stderr().lock(), ...)insrc/main.rs. - See
CHANGELOG.mdfor full details.
v0.1.8
Highlights
- CI GitHub Actions fully green on all 3 OSes — Fixed two platform-specific race conditions that did not surface on macOS during local validation but blocked CI: ubuntu-latest (
signal_test::shutdown_message_on_stderrexit 101) and windows-latest (atomic::create_backup_and_retentionexit 1) - POSIX signal-safety compliance —
eprintln!removed from SIGINT/SIGTERM signal handlers per POSIX.1-2017signal-safety(7). Shutdown message is now emitted by the main thread. - Windows backup fsync is now best-effort — New
platform::fsync_file_best_effortlogs a warning and continues when antivirus holds a read handle on%TEMP%files. Primary write path still uses strictfsync_file. - CI matrix pinned to
windows-2025-vs2026— Silences the windows-latest redirect NOTICE and prevents future runner changes from breaking the build.
Fixed (GAP 17 - signal handler POSIX safety)
signal_test::shutdown_message_on_stderrno longer fails on Linux CI — Removedeprintln!from the SIGINT and SIGTERM signal handlers. Per POSIX.1-2017signal-safety(7), stdio functions are NOT async-signal-safe. Rust'sstd::io::stderr()uses a globalMutexthat can deadlock or lose buffered output when the signal arrives while another thread holds the lock. On glibc Linux, the race was deterministic: stderr was empty. The shutdown message is now emitted by the main thread insrc/main.rswhen it observesis_shutdown() == trueafteratomwrite::runreturns, which is the only async-signal-safe way to guarantee the message reaches the captured stderr pipe before the process exits. The Windowsctrlcpath still emits the message inline because ctrlc handlers run in a normal thread.
Fixed (GAP 18 - Windows backup fsync)
atomic::tests::create_backup_and_retentionno longer fails on Windows CI — Addedplatform::fsync_file_best_effortwhich logs a warning and continues. On Windows, antivirus products (Windows Defender, third-party AV) transiently hold a read handle on files in%TEMP%withFILE_SHARE_READbut withoutFILE_SHARE_WRITE, which causesFlushFileBuffersto returnERROR_ACCESS_DENIED(os error 5). Only the backup-durability fsync is best-effort; the primary write path still uses the strictfsync_filebecause the backup itself has already been created viafs::copyby the time fsync runs.
Fixed (CI matrix)
- Pinned to
windows-2025-vs2026— The matrix entry for Windows was changed fromwindows-latesttowindows-2025-vs2026(its successor before the June 15 2026 GitHub-hosted runner migration). This silences the "windows-latest requests are being redirected" NOTICE and prevents unexpected runner changes from breaking the build.
Validation
cargo build --all-features: PASScargo clippy --all-features --all-targets -- -D warnings: PASScargo test --all-features: 302/302 tests PASS (all 5signal_testcases pass;atomic::create_backup_and_retentionpasses)cargo fmt -- --check: PASScargo audit: PASS (no vulnerabilities, no--ignoreflag)cargo deny check: PASS (advisories, bans, licenses, sources all OK)cargo build --release: PASS- CI ubuntu-latest, macos-latest, windows-2025-vs2026: pending confirmation on push
Notes
- v0.1.8 is a NON-BREAKING change. No public API was modified.
- The signal-handler change is internal: external consumers that relied on the shutdown message appearing on stderr continue to see it; it is now emitted by the main thread instead of the signal handler.
- The Windows backup fsync change is internal: backup files are still created and atomic; the only difference is that the durability flush for backup metadata is best-effort. If a future user reports data loss on backup, we can re-tighten the fsync.
See CHANGELOG.md for full details.