Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Section titled “[Unreleased]”Changed (website)
Section titled “Changed (website)”- Landing page leads with the consequence instead of the mechanism — the hero subheadline now names the failure mode (“if two processes write the same file, one silently loses data”) rather than listing four mechanisms, and the primary hero action is the copyable
go getcommand. Feature titles state outcomes (“No silent overwrites”, “Survives power loss”) instead of implementation names. - Meta description rewritten for search results and social previews: crash-safe framing instead of a TOCTOU and fsync mechanism list.
- Module path is now a single constant (
siteConfig.modulePath) — the hero snippet, the copyablego getcommand, the GitHub link, the pkg.go.dev link, and the Starlight docs meta description all derive from it. The module path was previously rebuilt by string-replacingsiteConfig.githubin two separate files, a smaller copy of the same split-brain bug. - Remaining corporate-register copy replaced — the feature grid subtitle now describes what the library does instead of “every decision optimized for…”, and the README lead names the three concrete failure modes rather than a mechanism list.
Added (website)
Section titled “Added (website)”- Landing page FAQ — answers “why not a temp file plus rename?”, “what does a write cost?”, “what happens when two writers race?”, and “when should I not use this?”.
- Build-time verification gate (
website/scripts/verify-build.mjs, run frompostbuild) — asserts the hero code shown indist/index.htmlis byte-identical to the code its copy button copies, that the install command matches thego.modmodule path, and that every[data-copy]button carries a non-empty payload. A regression of the hero split brain now fails the build. pnpm run validate— runshtml-validateon the built landing page; wired into the Website CI workflow.- i18n content collection (
src/content/i18n/en.json, intentionally empty) — silences theThe collection "i18n" does not exist or is emptywarning Starlight emitted on every build.
Fixed (website)
Section titled “Fixed (website)”- Landing page hero sample did not compile — the rendered snippet called
atomicwrite.Write(path, newData, fp), butWritetakes only(path, data); the copy button copied the correctWriteVerifiedcall, so the visible code and the copied code disagreed. Both now derive from a single token list inwebsite/src/data/hero-code.ts, so they cannot drift again. - README attributed
ErrConcurrentModificationto the wrong function — it claimedWritereturns it on fingerprint mismatch, butWritetakes no fingerprint. Corrected toWriteVerified. - Copy buttons survive navigation and announce success —
copy-code.jsnow installs one delegated click listener instead of binding per element, so it cannot double-bind across ClientRouter navigations, and it writes “Copied to clipboard” into arole="status"live region for screen readers.
Removed (website)
Section titled “Removed (website)”website/src/styles/global.out.css— a 44 KB generated Tailwind output with no references anywhere in the build; a leftover from an older CLI workflow.
Fixed (CI)
Section titled “Fixed (CI)”astro checkrequires TypeScript 6.x —websitepinnedtypescript^7.0.2→^6.0.3(and regeneratedpnpm-lock.yaml). TS7’s native compiler dropped the programmatic API thatastro checkdepends on. Shipped in the v0.5.2 tag but missed from its changelog section.- golangci-lint GitHub Action upgraded v6 → v7 (SHA-pinned,
version: v2.13.2explicit) — action v6 cannot install golangci-lint v2, andversion: latestresolves to the ancient v1.x line. Shipped in the v0.5.2 tag but missed from its changelog section. - Website deploys unblocked — the
FIREBASE_SERVICE_ACCOUNT_LARS_SOFTWARErepository secret was missing, so every CI deploy failed at the final step and the live site was frozen at the 0.4.0 content since 2026-07-26. A key for the sharedgithub-website-deploy@lars-softwareservice account is now registered, and the site is live again with current content.
Added (CI)
Section titled “Added (CI)”workflow_dispatchtrigger on the Website workflow so deploys can be run manually without pushing awebsite/**change.
[0.5.2] - 2026-09-17
Section titled “[0.5.2] - 2026-09-17”Changed
Section titled “Changed”- Toolchain: Go 1.26.5 → 1.26.7 —
go.modand.golangci.ymlaligned. No language or API changes; a toolchain maintenance/security bump only. - Dependency bumps —
gofrs/flockv0.13.0 → v0.13.1 andgolang.org/x/sys(indirect) v0.47.0 → v0.48.0. No API changes. golangci-lintconfig realigned to the v2.x schema — the gating check had drifted from the installed linter versions; findings from linters that no longer match this codebase’s patterns are silenced, and the now-unnecessary//nolint:goseccomments were removed (no behavior change).
Fixed (website & CI)
Section titled “Fixed (website & CI)”- Website HTML cache rule never matched cleanUrls pages — the
**/*.htmlrule missed directory-style URLs, so HTML fell back to Firebase’s defaultmax-age=3600(the same stale-content bug class behind the 2026-09-03 go-output outage).Cache-Control: public, max-age=0, must-revalidatemoved into the**catch-all, which Firebase applies last per header. - Website deploy pipeline repaired — pnpm is enabled via corepack before
setup-node(itscache: pnpmneeds the binary, which runners don’t preinstall),pnpm execreplacespnpm dlx(which ran unpinned latest tooling and crashedastro check), andfirebase-toolsinstalls vianpm i -gso its bin is onPATH.
Added (CI)
Section titled “Added (CI)”- Dependabot config — weekly grouped minor/patch updates for Go modules, GitHub Actions, and npm, capped at 5 open PRs per ecosystem.
[0.5.1] - 2026-08-15
Section titled “[0.5.1] - 2026-08-15”- Windows builds now compile (
rename_windows.go) —isRetryableRenamereferencedsyscall.ERROR_SHARING_VIOLATION, which Go’s stdlibsyscallpackage does not define on windows (it only exports a small errno subset). EveryGOOS=windowsbuild of this library — direct or transitive — failed withundefined: syscall.ERROR_SHARING_VIOLATION. The constant is now defined locally by value (syscall.Errno(32), the WinAPIERROR_SHARING_VIOLATIONcode), preserving the intended retry-on-sharing-violation behavior.
[0.5.0] - 2026-08-14
Section titled “[0.5.0] - 2026-08-14”WriteWithPerm(path, data, perm)— atomic write with explicit file permissions (0.4.0’s API always used 0o644).
Changed
Section titled “Changed”- Test hygiene: zero-length
maketo satisfy themakezerolinter.
[0.4.0] - 2026-07-26
Section titled “[0.4.0] - 2026-07-26”WriteIfChanged(path, data) (changed bool, err error)— writes only if content differs from disk. Returnschanged=truewhen the file was written,falsewhen skipped (identical content). This is the idiomatic primitive for config-file writers and code generators that must not produce spurious diffs on re-runs: no content change means no file mutation, no mtime bump, no file-watcher trigger. ComposesFingerprintFile+WriteVerified; first-write uses plainWrite(no prior content to protect).
Changed
Section titled “Changed”- BREAKING:
Write/WriteFuncAPI split (atomicwrite.go) — the previousWrite(path, data, fingerprint)mixed two distinct intents behind a zero-valueFingerprint{}, which silently skipped TOCTOU verification (a footgun). The API is now split by intent:Write(path, data)/WriteFunc(path, fn)— plain atomic write, no TOCTOU check (the common case).WriteVerified(path, data, fingerprint)/WriteFuncVerified(path, fn, fingerprint)— atomic write WITH fingerprint verification. A zero-value fingerprint here means “the file must NOT already exist” — a concurrent creation is treated as a conflict and fails, rather than being silently ignored.
- This is a source-incompatible change: every call site passing
Fingerprint{}to skip verification migrates toWrite/WriteFunc; every call site passing a real fingerprint migrates to the*Verifiedvariant. All in-repo consumers were updated.
Migration
Section titled “Migration”| Before | After |
|---|---|
Write(path, data, Fingerprint{}) |
Write(path, data) |
Write(path, data, fp) (real fingerprint) |
WriteVerified(path, data, fp) |
WriteFunc(path, fn, Fingerprint{}) |
WriteFunc(path, fn) |
WriteFunc(path, fn, fp) (real fingerprint) |
WriteFuncVerified(path, fn, fp) |
Fixed (website & docs)
Section titled “Fixed (website & docs)”- Website docs taught the removed API —
api-reference.mdx,getting-started/quick-start.mdx(4 examples),getting-started/installation.mdx,guides/error-handling.mdx, the landing-page hero code (hero-code.ts), and the “How it works” steps (sections.ts) all used the removedWrite(path, data, fingerprint)3-arg signature. Rewritten to the currentWrite/WriteVerified/WriteIfChanged/WriteFunc/WriteFuncVerifiedsplit. - Dual changelog split brain —
website/src/content/docs/changelog.mdxduplicated rootCHANGELOG.mdand had already diverged (no[Unreleased], old signatures). The website page is now generated from the root file bywebsite/scripts/sync-changelog.mjs(single source of truth, wired intoprebuild/predev). docs/DOMAIN_LANGUAGE.mddocumented the removed.bakpattern as a value object and event. Corrected to reflect single-rename atomicity and the full write-API split.pulse-dotanimation ignoredprefers-reduced-motion— the reduced-motion media query only targeted[data-animate]. Now disables.animate-pulse-dottoo (accessibility: vestibular disorders).
Added (website & CI)
Section titled “Added (website & CI)”- Hash-based Content Security Policy —
website/scripts/fix-csp.mjspost-build patcher injects a per-file CSP<meta>into every built HTML page. Inline scripts are allow-listed by SHA-256 hash (no'unsafe-inline'forscript-src);style-src 'unsafe-inline'is retained for Tailwind’s inlined critical CSS. - CI/CD pipelines —
.github/workflows/ci.yml(Govet+build+test -race+golangci-lint) and.github/workflows/website.yml(typecheck+buildon every change; deploy to Firebase Hostinglivechannel onmaster). - Changelog sync hook —
pnpm run sync:changelogand automaticprebuild/predevgeneration of the website changelog page.
Changed (website & docs)
Section titled “Changed (website & docs)”CONTRIBUTING.md— added awebsite/section (build/dev/deploy commands, the generated-changelog note), corrected the stale “No flake.nix” claim (the website has one), and documentedgolangci-lintas the gating check.
Removed (website)
Section titled “Removed (website)”- Dead website code — unused
comparisonsarray (sections.ts), unusedComparisonIteminterface (types.ts), and the unusedfade-in-upkeyframe + theme variable (global.css).comparisonMatrix(which IS rendered) is retained.
Added (website accessibility & social)
Section titled “Added (website accessibility & social)”- OG / social-share image — static 1200×630
og-image.png(generated fromog-image.svgsource) withog:image,twitter:image, andtwitter:card=summary_large_imagemeta tags on both the landing page and all Starlight docs pages. favicon.ico— multi-resolution (16/32/48/64px) ICO generated fromfavicon.svg, referenced via<link rel="alternate icon">for legacy browsers that only request.ico.- Lighthouse CI —
website/lighthouserc.jsonwith performance/accessibility/SEO budgets (desktop preset),@lhci/clidevDependency,pnpm run lighthousescript, andchromiumin the Nix devShell (CHROME_PATHset). Verified: Performance 100, Accessibility 100, Best-Practices 96, SEO 100. website/.editorconfig— standalone editor config for the website subtree (2-space indent for all web files,root = trueto decouple from the Go tab-based root config).
Fixed (website accessibility)
Section titled “Fixed (website accessibility)”- WCAG AA contrast failure —
--color-text-mutedwas#78716c(4.08:1 on the dark#0a0908background, below the 4.5:1 AA minimum). Dark mode lightened to#8b827a(5.28:1); light mode darkened to#6e6762(5.32:1) for comfortable margin. - Comparison matrix screen-reader inaccessible — check/X icons in
ComparisonSection.astrohad no text alternative. Each data cell now has a descriptivearia-label(e.g., “TOCTOU-safe, go-atomic-write: Yes”); feature-name cells changed to<th scope="row">; the~partial symbol markedaria-hidden="true".
Changed (website infra)
Section titled “Changed (website infra)”website/flake.nixpinned Node 24 — replacedpkgs.nodejs(Node 22 on unstable) withpkgs.nodejs_24across all apps and devShell, matching the.node-versionfile and AGENTS.md requirement.website/flake.nixapp metadata — addedmeta.descriptionandmeta.mainProgramto everymkApp(dev, build, preview, deploy);nix flake checkno longer warns about missing attributes.
[0.3.0] - 2026-07-23
Section titled “[0.3.0] - 2026-07-23”WriteFunc(path, fn, fingerprint)— streaming atomic writes via a callback. The callback receives abufio.Writer(64KB buffer) so callers can stream large or incrementally-produced content (JSON encoders, diagram renderers, etc.) without holding the full payload in memory. The temp file is fsync’d before atomic rename, and TOCTOU fingerprint verification works identically toWrite.- Comprehensive
WriteFunctests — first run, overwrite, large stream (400KB across 100 chunks), callback error cleanup, permission preservation, and no-leftover-files verification
Changed
Section titled “Changed”- Benchmark data generation extracted into a shared
benchDatahelper — eliminates duplicatedmake([]byte, size)+ fill-by-index across 4 benchmark functions .gitattributesadded for cross-platform line-ending normalization- Full marketing website launched with Astro + Starlight + Tailwind v4 at atomicwrite.lars.software
[0.2.0] - 2026-06-28
Section titled “[0.2.0] - 2026-06-28”- Removed
.baktwo-rename pattern — the oldatomicRenamedidpath→.bakthentmp→path, opening a window where the target didn’t exist. A concurrent reader could seeENOENT, and a crash between the two renames left the target missing. The library named “atomic-write” was not atomic. Now a singleos.Rename(tmp, path)is one atomic syscall on POSIX and atomically replaces on Windows viaMoveFileEx. - Concurrent writer corruption —
tmpPathwas computed before the lock (path + ".tmp"), so two concurrentWrite()calls to the same target both truncated and wrote the same staging file, interleaving bytes. Now each writer gets a unique temp file viacrypto/randsuffix (path + "." + randomHex + ".tmp"). - Concurrency test couldn’t catch the bug —
TestConcurrentWriteRACEused identical content from all writers, so byte interleaving was undetectable. Rewritten with 10 writers using divergent content (writer-0throughwriter-9) plus an integrity check verifying the final file is exactly one writer’s payload. - “Crash safety” doc claim was false — no
fsyncwas called anywhere. NowwriteAndSync()opens the temp file, writes data, callsfile.Sync(), and closes (with cleanup on error). On POSIX, the target directory is alsofsync’d after rename viasyncDir(). The package doc now says “atomic rename, and fsync for crash durability” — and means it.
fsyncfor crash durability — temp file is fsync’d before rename; target directory is fsync’d after rename (POSIX only; Windows has no equivalent directory sync)- Unique temp file names —
crypto/rand4-byte hex suffix prevents concurrent writer staging-file corruption - Platform-specific rename — split into
rename_unix.go(singlerename(2)+ directoryfsync) andrename_windows.go(retry loop onERROR_ACCESS_DENIED/ERROR_SHARING_VIOLATION, 5 retries with exponential backoff 1–16ms, matching Go’s owncmd/gofix for issue 36568) writeAndSync()— opens temp file, writes data, fsyncs, closes; cleans up temp file on any errorrandomSuffix()— generates random hex suffix for unique temp file namessyncDir()— opens and fsyncs the target directory after rename (POSIX only)TestWriteLeavesNoLeftoverFiles— verifies no.bakand no.tmpfiles remain after a successful write- Status report — full HTML dashboard at
docs/status/2026-06-28_03-32_atomicity-durability-overhaul.html
Changed
Section titled “Changed”- Package doc: “atomic rename for crash safety” → “atomic rename, and fsync for crash durability”
Write()now stages to a unique temp file instead of a sharedpath + ".tmp"atomicRename()no longer creates.bakfiles — singleos.Renamereplaces atomicallyTestConcurrentWriteRACErewritten: 10 writers, divergent content, integrity checkTestTempFileCleanedUpOnErrorusesfilepath.Globinstead of hardcoded temp path- AGENTS.md, README.md updated: new architecture, data flow, file structure, platform-specific behavior, removed all
.bakreferences go.sumtidied — removed stale testify/go-spew/go-difflib/yaml.v3 checksums
Removed
Section titled “Removed”TestWriteCreatesBackup— tested the.bakpattern as a feature; the pattern was a bug.bakfile creation — no longer created alongside the target
Caveats
Section titled “Caveats”- Windows rename retry loop and
FlushFileBufferscompile via build tags but are untested on real Windows hardware - No POSIX-equivalent directory fsync on Windows — the directory entry after rename may not be durable after a crash
- No stale temp file cleanup — crashed writes leave
.tmpfiles (unique names prevent interference but don’t self-clean)
[0.1.1] - 2026-06-03
Section titled “[0.1.1] - 2026-06-03”Changed
Section titled “Changed”- LICENSE changed from PROPRIETARY to MIT — the README already stated MIT; the LICENSE file now matches
.golangci.ymlnow permitsgithub.com/cespare/xxhash/v2andgithub.com/gofrs/flockin thedepguardallow list (both are intentional runtime dependencies, not candidates for replacement)- Tests use
//nolint:gosecannotations with rationale foros.ReadFile/os.WriteFilecalls that operate exclusively ont.TempDir()paths TestConcurrentWriteRACEnow callst.Parallel()to satisfyparalleltestand to actually run in parallel with other tests- Benchmark helpers call
b.Helper()so failures are attributed to the caller - Variable names lengthened for
varnamelencompliance:fp→fingerprint,wg→waitGroup,h→hasher,mb→megabyte - Inline
if err := …; err != nil { … }blocks in tests refactored to plain assignment plusiffornoinlineerrcompliance - Documentation formatting and accuracy fixes across README, CHANGELOG, CONTRIBUTING, DOMAIN_LANGUAGE, and AGENTS.md
errcheck:h.Writereturn value is now explicitly discarded (_, _ = hasher.Write(…)) in the streaming benchmark*.bakadded to.gitignore— backup files from atomic writes were showing as untracked
[0.1.0] - 2026-06-03
Section titled “[0.1.0] - 2026-06-03”- TOCTOU-safe file writes via xxhash64 fingerprint verification
- Cross-platform file locking (
flockon Unix,LockFileExon Windows) viagofrs/flock - Atomic rename with
.bakbackup of previous content Fingerprinttype ([8]byte) withIsZero()andMatches()methodsFingerprintFile— computes fingerprint from file path (zero-value for nonexistent files)FingerprintFromBytes— computes fingerprint from raw bytesWrite(path, data, fingerprint)— main API with TOCTOU protectionErrConcurrentModificationsentinel error for fingerprint mismatch detection- File permission preservation from existing file (defaults to
0644for new files) - xxhash64 vs SHA-256 benchmarks demonstrating ~11× speed advantage
- Unit tests, permission preservation tests, and concurrent writer contention test