Skip to content

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.

  • 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 get command. 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 copyable go get command, 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-replacing siteConfig.github in 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.
  • 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 from postbuild) — asserts the hero code shown in dist/index.html is byte-identical to the code its copy button copies, that the install command matches the go.mod module 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 — runs html-validate on the built landing page; wired into the Website CI workflow.
  • i18n content collection (src/content/i18n/en.json, intentionally empty) — silences the The collection "i18n" does not exist or is empty warning Starlight emitted on every build.
  • Landing page hero sample did not compile — the rendered snippet called atomicwrite.Write(path, newData, fp), but Write takes only (path, data); the copy button copied the correct WriteVerified call, so the visible code and the copied code disagreed. Both now derive from a single token list in website/src/data/hero-code.ts, so they cannot drift again.
  • README attributed ErrConcurrentModification to the wrong function — it claimed Write returns it on fingerprint mismatch, but Write takes no fingerprint. Corrected to WriteVerified.
  • Copy buttons survive navigation and announce success — copy-code.js now 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 a role="status" live region for screen readers.
  • 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.
  • astro check requires TypeScript 6.x — website pinned typescript ^7.0.2 → ^6.0.3 (and regenerated pnpm-lock.yaml). TS7’s native compiler dropped the programmatic API that astro check depends 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.2 explicit) — action v6 cannot install golangci-lint v2, and version: latest resolves 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_SOFTWARE repository 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 shared github-website-deploy@lars-software service account is now registered, and the site is live again with current content.
  • workflow_dispatch trigger on the Website workflow so deploys can be run manually without pushing a website/** change.
  • Toolchain: Go 1.26.5 → 1.26.7 — go.mod and .golangci.yml aligned. No language or API changes; a toolchain maintenance/security bump only.
  • Dependency bumps — gofrs/flock v0.13.0 → v0.13.1 and golang.org/x/sys (indirect) v0.47.0 → v0.48.0. No API changes.
  • golangci-lint config 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:gosec comments were removed (no behavior change).
  • Website HTML cache rule never matched cleanUrls pages — the **/*.html rule missed directory-style URLs, so HTML fell back to Firebase’s default max-age=3600 (the same stale-content bug class behind the 2026-09-03 go-output outage). Cache-Control: public, max-age=0, must-revalidate moved into the ** catch-all, which Firebase applies last per header.
  • Website deploy pipeline repaired — pnpm is enabled via corepack before setup-node (its cache: pnpm needs the binary, which runners don’t preinstall), pnpm exec replaces pnpm dlx (which ran unpinned latest tooling and crashed astro check), and firebase-tools installs via npm i -g so its bin is on PATH.
  • Dependabot config — weekly grouped minor/patch updates for Go modules, GitHub Actions, and npm, capped at 5 open PRs per ecosystem.
  • Windows builds now compile (rename_windows.go) — isRetryableRename referenced syscall.ERROR_SHARING_VIOLATION, which Go’s stdlib syscall package does not define on windows (it only exports a small errno subset). Every GOOS=windows build of this library — direct or transitive — failed with undefined: syscall.ERROR_SHARING_VIOLATION. The constant is now defined locally by value (syscall.Errno(32), the WinAPI ERROR_SHARING_VIOLATION code), preserving the intended retry-on-sharing-violation behavior.
  • WriteWithPerm(path, data, perm) — atomic write with explicit file permissions (0.4.0’s API always used 0o644).
  • Test hygiene: zero-length make to satisfy the makezero linter.
  • WriteIfChanged(path, data) (changed bool, err error) — writes only if content differs from disk. Returns changed=true when the file was written, false when 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. Composes FingerprintFile + WriteVerified; first-write uses plain Write (no prior content to protect).
  • BREAKING: Write / WriteFunc API split (atomicwrite.go) — the previous Write(path, data, fingerprint) mixed two distinct intents behind a zero-value Fingerprint{}, 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 to Write/WriteFunc; every call site passing a real fingerprint migrates to the *Verified variant. All in-repo consumers were updated.
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)
  • 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 removed Write(path, data, fingerprint) 3-arg signature. Rewritten to the current Write / WriteVerified / WriteIfChanged / WriteFunc / WriteFuncVerified split.
  • Dual changelog split brain — website/src/content/docs/changelog.mdx duplicated root CHANGELOG.md and had already diverged (no [Unreleased], old signatures). The website page is now generated from the root file by website/scripts/sync-changelog.mjs (single source of truth, wired into prebuild/predev).
  • docs/DOMAIN_LANGUAGE.md documented the removed .bak pattern as a value object and event. Corrected to reflect single-rename atomicity and the full write-API split.
  • pulse-dot animation ignored prefers-reduced-motion — the reduced-motion media query only targeted [data-animate]. Now disables .animate-pulse-dot too (accessibility: vestibular disorders).
  • Hash-based Content Security Policy — website/scripts/fix-csp.mjs post-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' for script-src); style-src 'unsafe-inline' is retained for Tailwind’s inlined critical CSS.
  • CI/CD pipelines — .github/workflows/ci.yml (Go vet + build + test -race + golangci-lint) and .github/workflows/website.yml (typecheck + build on every change; deploy to Firebase Hosting live channel on master).
  • Changelog sync hook — pnpm run sync:changelog and automatic prebuild/predev generation of the website changelog page.
  • CONTRIBUTING.md — added a website/ section (build/dev/deploy commands, the generated-changelog note), corrected the stale “No flake.nix” claim (the website has one), and documented golangci-lint as the gating check.
  • Dead website code — unused comparisons array (sections.ts), unused ComparisonItem interface (types.ts), and the unused fade-in-up keyframe + theme variable (global.css). comparisonMatrix (which IS rendered) is retained.
  • OG / social-share image — static 1200×630 og-image.png (generated from og-image.svg source) with og:image, twitter:image, and twitter:card=summary_large_image meta tags on both the landing page and all Starlight docs pages.
  • favicon.ico — multi-resolution (16/32/48/64px) ICO generated from favicon.svg, referenced via <link rel="alternate icon"> for legacy browsers that only request .ico.
  • Lighthouse CI — website/lighthouserc.json with performance/accessibility/SEO budgets (desktop preset), @lhci/cli devDependency, pnpm run lighthouse script, and chromium in the Nix devShell (CHROME_PATH set). 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 = true to decouple from the Go tab-based root config).
  • WCAG AA contrast failure — --color-text-muted was #78716c (4.08:1 on the dark #0a0908 background, 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.astro had no text alternative. Each data cell now has a descriptive aria-label (e.g., “TOCTOU-safe, go-atomic-write: Yes”); feature-name cells changed to <th scope="row">; the ~ partial symbol marked aria-hidden="true".
  • website/flake.nix pinned Node 24 — replaced pkgs.nodejs (Node 22 on unstable) with pkgs.nodejs_24 across all apps and devShell, matching the .node-version file and AGENTS.md requirement.
  • website/flake.nix app metadata — added meta.description and meta.mainProgram to every mkApp (dev, build, preview, deploy); nix flake check no longer warns about missing attributes.
  • WriteFunc(path, fn, fingerprint) — streaming atomic writes via a callback. The callback receives a bufio.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 to Write.
  • Comprehensive WriteFunc tests — first run, overwrite, large stream (400KB across 100 chunks), callback error cleanup, permission preservation, and no-leftover-files verification
  • Benchmark data generation extracted into a shared benchData helper — eliminates duplicated make([]byte, size) + fill-by-index across 4 benchmark functions
  • .gitattributes added for cross-platform line-ending normalization
  • Full marketing website launched with Astro + Starlight + Tailwind v4 at atomicwrite.lars.software
  • Removed .bak two-rename pattern — the old atomicRename did path→.bak then tmp→path, opening a window where the target didn’t exist. A concurrent reader could see ENOENT, and a crash between the two renames left the target missing. The library named “atomic-write” was not atomic. Now a single os.Rename(tmp, path) is one atomic syscall on POSIX and atomically replaces on Windows via MoveFileEx.
  • Concurrent writer corruption — tmpPath was computed before the lock (path + ".tmp"), so two concurrent Write() calls to the same target both truncated and wrote the same staging file, interleaving bytes. Now each writer gets a unique temp file via crypto/rand suffix (path + "." + randomHex + ".tmp").
  • Concurrency test couldn’t catch the bug — TestConcurrentWriteRACE used identical content from all writers, so byte interleaving was undetectable. Rewritten with 10 writers using divergent content (writer-0 through writer-9) plus an integrity check verifying the final file is exactly one writer’s payload.
  • “Crash safety” doc claim was false — no fsync was called anywhere. Now writeAndSync() opens the temp file, writes data, calls file.Sync(), and closes (with cleanup on error). On POSIX, the target directory is also fsync’d after rename via syncDir(). The package doc now says “atomic rename, and fsync for crash durability” — and means it.
  • fsync for 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/rand 4-byte hex suffix prevents concurrent writer staging-file corruption
  • Platform-specific rename — split into rename_unix.go (single rename(2) + directory fsync) and rename_windows.go (retry loop on ERROR_ACCESS_DENIED/ERROR_SHARING_VIOLATION, 5 retries with exponential backoff 1–16ms, matching Go’s own cmd/go fix for issue 36568)
  • writeAndSync() — opens temp file, writes data, fsyncs, closes; cleans up temp file on any error
  • randomSuffix() — generates random hex suffix for unique temp file names
  • syncDir() — opens and fsyncs the target directory after rename (POSIX only)
  • TestWriteLeavesNoLeftoverFiles — verifies no .bak and no .tmp files remain after a successful write
  • Status report — full HTML dashboard at docs/status/2026-06-28_03-32_atomicity-durability-overhaul.html
  • 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 shared path + ".tmp"
  • atomicRename() no longer creates .bak files — single os.Rename replaces atomically
  • TestConcurrentWriteRACE rewritten: 10 writers, divergent content, integrity check
  • TestTempFileCleanedUpOnError uses filepath.Glob instead of hardcoded temp path
  • AGENTS.md, README.md updated: new architecture, data flow, file structure, platform-specific behavior, removed all .bak references
  • go.sum tidied — removed stale testify/go-spew/go-difflib/yaml.v3 checksums
  • TestWriteCreatesBackup — tested the .bak pattern as a feature; the pattern was a bug
  • .bak file creation — no longer created alongside the target
  • Windows rename retry loop and FlushFileBuffers compile 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 .tmp files (unique names prevent interference but don’t self-clean)
  • LICENSE changed from PROPRIETARY to MIT — the README already stated MIT; the LICENSE file now matches
  • .golangci.yml now permits github.com/cespare/xxhash/v2 and github.com/gofrs/flock in the depguard allow list (both are intentional runtime dependencies, not candidates for replacement)
  • Tests use //nolint:gosec annotations with rationale for os.ReadFile/os.WriteFile calls that operate exclusively on t.TempDir() paths
  • TestConcurrentWriteRACE now calls t.Parallel() to satisfy paralleltest and to actually run in parallel with other tests
  • Benchmark helpers call b.Helper() so failures are attributed to the caller
  • Variable names lengthened for varnamelen compliance: fp → fingerprint, wg → waitGroup, h → hasher, mb → megabyte
  • Inline if err := …; err != nil { … } blocks in tests refactored to plain assignment plus if for noinlineerr compliance
  • Documentation formatting and accuracy fixes across README, CHANGELOG, CONTRIBUTING, DOMAIN_LANGUAGE, and AGENTS.md
  • errcheck: h.Write return value is now explicitly discarded (_, _ = hasher.Write(…)) in the streaming benchmark
  • *.bak added to .gitignore — backup files from atomic writes were showing as untracked
  • TOCTOU-safe file writes via xxhash64 fingerprint verification
  • Cross-platform file locking (flock on Unix, LockFileEx on Windows) via gofrs/flock
  • Atomic rename with .bak backup of previous content
  • Fingerprint type ([8]byte) with IsZero() and Matches() methods
  • FingerprintFile — computes fingerprint from file path (zero-value for nonexistent files)
  • FingerprintFromBytes — computes fingerprint from raw bytes
  • Write(path, data, fingerprint) — main API with TOCTOU protection
  • ErrConcurrentModification sentinel error for fingerprint mismatch detection
  • File permission preservation from existing file (defaults to 0644 for new files)
  • xxhash64 vs SHA-256 benchmarks demonstrating ~11× speed advantage
  • Unit tests, permission preservation tests, and concurrent writer contention test