Changelog
2.4.1 (2026-10-07)
Section titled “2.4.1 (2026-10-07)”Bug Fixes
Section titled “Bug Fixes”- website: stop card grids overflowing into the right-hand TOC (7c4ea79)
2.4.0 (2026-10-06)
Section titled “2.4.0 (2026-10-06)”- NFC Unicode normalization in
pathKey()(filesystem.go) — all path comparisons now applynorm.NFC.String()before case-folding, fixing the invisible mismatch where macOS stores filenames as NFD (decomposed) but user-configured paths are NFC (composed). Without this, exclude-path matching, debounce deduplication, and gitignore prefix checks silently fail on any non-ASCII filename (café,München,東京). Idempotent on ASCII, zero impact on pure-ASCII paths. normalizePath()helper (filesystem.go) —filepath.Clean(filepath.Abs(path))applied to all path resolution sites (New,Add,Remove,WithExcludePaths,FilterExcludePaths), preventing trailing-slash,.., and redundant-separator mismatches between operations.addToWatchList()helper (watcher.go) — single point of mutation for the parallelwatchListslice andwatchListKeysmap, eliminating the desync risk from updating them independently at 5+ call sites.Remove()prunes subtree keys incrementally viadelete()rather than a full O(n) rebuild.- Poll loop case-awareness (
watcher_poll.go) —pollWalkDirandpollDetectChangesnow usepathKey()for snapshot/current map keys, preventing phantom Create+Remove events on case-only renames on case-insensitive filesystems. - Gitignore matcher case-awareness (
watcher_gitignore.go) —gitignoreCache.load()andshouldSkipByGitignore()now use canonical pathKeys for map storage and ancestor-prefix matching, closing the gap where gitignore rules were silently bypassed on macOS. - NFC normalization tests (
filesystem_test.go) — 6 tests covering NFC vs NFD pathKey equality, ASCII idempotency, exclude-path matching, and debounce key collision. - Poll + gitignore case-awareness tests (
filesystem_poll_gitignore_test.go) — 7 tests covering poll snapshot canonical keys, original-path preservation in event emission, case-only rename phantom-event prevention, and gitignore ancestor-prefix matching. FilterCaseInsensitive(inner Filter)wrapper (filter.go) — lowercases + NFC-normalizes the event path before delegating to an inner filter, enabling filter-level case-insensitivity without changing the watcher-wide mode.Stats.CaseSensitivityfield + Prometheus gauge (watcher.go,metrics.go) — the resolved case-sensitivity mode is now exposed viaStats()and as afilewatcher_case_sensitivitygauge (0=case-sensitive, 1=case-insensitive, 2=auto).pathKey()benchmarks (benchmark_test.go) —BenchmarkPathKey_{ASCII,UnicodeNFC,UnicodeNFD}_{CaseSensitive,CaseInsensitive}measuring NFC normalization cost: ASCII is ~26 ns/0 allocs (free), pre-composed NFC ~140 ns/0 allocs, decomposed NFD ~1 µs/3 allocs (macOS fs paths only).FuzzPathKey(fuzz_test.go) — fuzz target provingpathKey()never panics and is deterministic + idempotent across combining marks, emoji, ZWJ sequences, and invalid UTF-8.- README “Filesystem Compatibility” section — documents case-sensitivity awareness, NFC normalization, and
FilterCaseInsensitivefor end users. cleanPath()helper (filesystem.go) — best-effort path cleaning without error return. Replacesnormalized, _ := normalizePath(path)idiom inFilterExcludePathsandWithExcludePaths, eliminating the error-discarding code smell.Stats.CaseSensitivityModefield (watcher.go) — additiveFilesystemCaseSensitivityenum field alongside the existingCaseSensitivity string. Provides type-safe enum access without parsing, and enables the Prometheus gauge to callGaugeValue()directly.FilterCaseSensitive(inner Filter)wrapper (filter.go) — NFC-normalizes the event path without case-folding. Symmetric withFilterCaseInsensitive. Useful on macOS where event paths arrive as NFD but case-sensitive matching is needed.EffectiveCaseSensitivity()method (watcher.go) — returns the resolvedFilesystemCaseSensitivityenum without the overhead of a fullStats()call. Type-safe alternative toStats().CaseSensitivity(string).- GaugeValue + consistency tests (
filesystem_test.go) —TestFilesystemCaseSensitivity_GaugeValue,TestStats_CaseSensitivityFieldsConsistent,TestStats_CaseSensitivityModeReflectsMode, andTestCleanPathverify the wired-up gauge encoding and string/enum field consistency. - FilterCaseSensitive + EffectiveCaseSensitivity tests (
filesystem_test.go) — verify NFC normalization without case-folding, case preservation, and correct auto-detection resolution. - Edge-case tests (
watcher_test.go) —TestWatcher_Add_TrailingSlashNormalizedandTestWatcher_Remove_DeeplyNestedUnicodeSubtree(3-level Unicode tree: café/München/東京) verify path normalization and subtree key cleanup. - Filter + emoji benchmarks (
benchmark_test.go) —BenchmarkFilterCaseInsensitive(~250 ns/1 alloc),BenchmarkFilterCaseSensitive(~115 ns/0 allocs), andBenchmarkPathKey_EmojiZWJ_CaseSensitive(~170 ns/0 allocs) measuring wrapper overhead and deep Unicode normalization cost. WithMaxWatchesSafetyFraction(float64)option (options.go) — applies a safety fraction to the auto-detected inotify watch limit, leaving headroom for other processes on shared or multi-tenant machines. Default 1.0 (no reduction); recommended 0.75 for shared environments. Only affects auto-detected limits; explicitWithMaxWatches(n)values are used as-is.WithWatchFilteredDirectories(bool)option (options.go) — controls whether newly created directories whoseCreateevent was filtered out are still added to the watcher. Defaulttrue(preserves existing behavior). Whenfalse, filtered directories are not watched, saving inotify budget for consumers that truly want to ignore subtrees.WithEventChannelMode(EventChannelMode)option +EventChannelModeenum (options.go) —EventChannelBlocking(default, preserves backpressure semantics) andEventChannelDropOnFull(drops events when the channel is full instead of blocking the watch loop). DropOnFull is valuable for consumers that prefer losing events over blocking the watcher.MiddlewareDeduplicateCaseInsensitivemiddleware (middleware.go) — case-insensitive variant ofMiddlewareDeduplicatethat lowercases dedup keys. Use on case-insensitive filesystems (NTFS, APFS) where/File.goand/file.goare the same file.FilterIgnoreDirsCaseInsensitivefilter (filter.go) — case-insensitive variant ofFilterIgnoreDirsfor event-time directory-name matching on case-insensitive filesystems.Stats.EventsDroppedByMiddlewarefield (watcher.go) — counts events dropped by middleware (rate limiting, dedup, circuit breaker, etc.) viaatomic.Boolemit tracking inemitEvent. Surfaces middleware drops that were previously invisible in stats.Stats.EventsDroppedByBackpressurefield (watcher.go) — counts events dropped because the event channel was full whenDropOnFullmode is active.Stats.ErrorsDroppedfield (watcher.go) — counts errors dropped because the error channel was full. Gives consumers a way to detect they need to increaseWithBufferor read errors more frequently.- Prometheus metrics for drops (
metrics.go) —filewatcher_events_dropped_by_middleware_totalandfilewatcher_errors_dropped_totalcounters added toPrometheusCollector. - MiddlewareBatch timer flush error routing (
middleware.go) — timer-flush errors are now stored inbatchState.flushErrand returned on the next event, routing through the watcher’shandleErrorpipeline instead of being silently logged. - Symlink cycle detection (
watcher_walk.go) —WithFollowSymlinks(true)now tracks resolved symlink targets in asymlinkVisitedmap, preventing infinite recursion and duplicate watches when symlinks point to ancestors or already-watched directories. - Content size cap for
FilterGeneratedCodeFull(filter_gogen.go) — files larger than 10 MiB (defaultGogenMaxFileSize) are skipped for content-based generated-code detection, preventing synchronous I/O stalls on large files. - Troubleshooting guide sections (
Troubleshooting.md) — 5 new sections: Polling Mode Limitations, Slow Consumers and Event Loss, Middleware Drops Not Visible, Content Hashing Performance, Symlink Cycles. Stats.WatchBudgetCapfield (watcher.go) — shows the effective watch budget cap after the safety fraction is applied.WatchLimitnow shows the raw system-detected limit before the fraction.filewatcher_events_dropped_by_backpressure_total+filewatcher_watch_budget_capPrometheus metrics (metrics.go) — the backpressure drop counter and budget cap gauge are now exposed byPrometheusCollector.WithContentHashMaxSize(bytes int64)option (options.go) — configurable upper bound for content hashing. Files larger than this are skipped to avoid blocking the event loop. Default 10 MiB. Implicitly enables content hashing when set to a positive value.WithErrorBufferSize(n int)option (options.go) — decouples the error channel capacity from the event channel (WithBuffer). When not set, the error channel uses the same buffer size as the event channel.NewWatcherErrorWithStackconstructor (errors.go) — creates aWatcherErrorwith a caller-provided stack trace, useful when the underlying error originates in a different goroutine or deep in a syscall whereNewWatcherError’s auto-captured stack would be misleading.
-
WithDebug(nil)panic (options.go,watcher_internal.go) — the option doc says “If logger is nil,log/slog.Default()is used” but the implementation stored nil, causing a nil-pointer panic on the first debug log. Now honors the documented contract with a nil guard indebugLogas defense-in-depth. -
MiddlewareBatchduplicate emission on full batch (middleware.go) — when the batch reachedmaxSize, the first N-1 events were flushed AND the triggering event was also emitted individually vianext(ctx, event). The triggering event is already in the flushed batch, so the individual emission was a duplicate. Removed the spuriousnextcall. -
Polling mode ignores walk-time exclusions (
watcher_poll.go) —pollWalkDironly checkedshouldSkipDirbut nevershouldExcludePathorshouldSkipByGitignore. Now applies the same skip logic as the initial walk, including loading.gitignoreper directory. -
FilterExcludePathsandFilterGitignoresensitive to path form (filter.go) — relative paths passed by the consumer were compared against absolute event paths, causing silent filter misses. Both now normalize to absolute viafilepath.Absbefore comparison. -
MiddlewareDeduplicatedoes not normalize path keys (middleware.go) — dedup keys used rawevent.Path, so NFD/NFC variants (macOS) or case-only differences (NTFS/APFS) were not deduplicated. Now applies NFC normalization;MiddlewareDeduplicateCaseInsensitiveadds case-folding. -
Path validation in
Add/AddRecursive/Watch()(watcher.go) — invalid paths were silently added tofailedPathsand retried by self-heal forever. NowvalidateDirExists()stats the path and returnsErrPathNotFoundimmediately for non-existent paths orErrPathNotDirfor files. -
Syscall error classification (
errors.go) —categorizeErrordid not recognizeos.ErrPermission,os.ErrNotExist, orsyscall.ENOTDIRas permanent, causing self-heal to retry permission-denied paths forever. Now classifies these as permanent;syscall.ENOSPCas transient. -
Case-insensitive
shouldSkipDir(watcher_walk.go) — walk-time directory-name matching was always case-sensitive, soBUILDandNode_Moduleswere not skipped on case-insensitive filesystems. Now lowercases comparisons whenCaseInsensitivemode is active. -
applyMaxWatchesFractionapplied to explicit limits (watcher.go) — the safety fraction was applied to allmaxWatches > 0, including explicitWithMaxWatches(n)values, contradicting the documentation. Now only applies to auto-detected limits.Reset()also preserves explicit settings instead of always re-detecting. -
EventsProcessedcounted dropped events (watcher_internal.go) — the counter was incremented before the channel send, so events dropped byDropOnFullbackpressure or aborted during shutdown were counted as “processed.” Now only increments on successful send. -
Dead code:
GaugeValue()never called (metrics.go,filesystem.go) — theGaugeValue()method was defined but never invoked. The Prometheus gauge used a separatecaseSensitivityGauge(string)function with its own switch/case, duplicating the gauge encoding. Now wired viaStats.CaseSensitivityMode, and the duplicatecaseSensitivityGauge()function is deleted. -
_error discarding code smell (filter.go,options.go) —FilterExcludePathsandWithExcludePathsusednormalized, _ := normalizePath(path), discarding the error with a comment. Replaced with thecleanPath()helper that encapsulates the best-effort intent. -
go.mod language version accidentally bumped to 1.27 (
go.mod) — an auto-commit raised the required Go version to 1.27, breaking builds on Go 1.26 toolchains. Restored the intendedgo 1.26.7floor.
Changed
Section titled “Changed”-
eventsProcessedsemantics (watcher_internal.go) — the counter now increments when an event reaches the event channel (insidetrackedEmit), not when the middleware chain returns nil. This is more correct: an event is “processed” when it’s emitted, not when middleware returns nil (which could happen even if the event was never sent, e.g., due to context cancellation). Events dropped by middleware are no longer counted as processed. -
Middleware drop tracking via
atomic.Bool(watcher_internal.go) —emitEventwraps the emit function with anatomic.Boolthat detects whether the middleware chain called emit. If middleware returns nil without calling emit, the event was dropped andEventsDroppedByMiddlewareis incremented. This replaces the removedexecuteHandlermethod. -
Remove()watchListKeys desync (watcher.go) —Remove()only deleted the exact path key fromwatchListKeys, leaving subtree path keys orphaned. Now deletes each pruned subtree key incrementally (O(removed), not O(n) rebuild), guaranteeing the map stays in sync after bulk subtree removal. -
normalizePath()best-effort cleaning onAbsfailure (filesystem.go) — whenfilepath.Absfails (e.g. unreadable cwd), the returned path is now stillfilepath.Clean’d so trailing slashes,.., and redundant separators are normalized.FilterExcludePaths/WithExcludePathsuse the always-cleaned result instead of swallowing the error and falling back to the raw path. -
golang.org/x/textpromoted to direct dependency (go.mod) — was an indirect dependency; now explicitly required forunicode/normNFC normalization inpathKey(). -
WatchLimitsemantics (watcher.go) — now shows the raw system-detected inotify limit before the safety fraction. The effective cap (after fraction) is exposed as the newStats.WatchBudgetCapfield. Consumers who previously readWatchLimitto see the effective cap should switch toWatchBudgetCap. -
hashFileacceptsmaxSizeparameter (filter.go) — the hardcoded 10 MiB cap is now a parameter.convertEventreceivesmaxHashSize int64instead ofcomputeHash bool; whenmaxHashSize > 0, hashing is enabled with that cap. -
FilesystemCaseSensitivitypromoted to Stable API (API_STABILITY.md) — the enum values are foundational (controlpathKey()behavior) and will not change. Adding new values (e.g.,CaseSensitivityProbedin v3) is backward-compatible.WithCaseSensitivityandFilterCaseInsensitiveremain Evolving. -
Prometheus gauge uses
GaugeValue()directly (metrics.go) — thefilewatcher_case_sensitivitygauge now readsstats.CaseSensitivityMode.GaugeValue()instead of parsing a string. Single source of truth for the gauge encoding. -
FilterGeneratedCodesqlc detection follows gogenfilter v3.6 semantics (filter_gogen.go,go.mod) — the gogenfilter dependency moved from v3.2 to v3.6.1: weak filenames such asmodels.goare no longer treated as sqlc-generated on their own. Strong*.sql.gopatterns still match by filename; content markers (// Code generated by sqlc. DO NOT EDIT.) are detected byFilterGeneratedCodeFullwithContentCheckEnabled.
[2.3.0] - 2026-07-27
Section titled “[2.3.0] - 2026-07-27”WithCleanupoption (options.go) — registers cleanup functions called onClose()after goroutines/channels are torn down. Cleared onReset(). Enables lifecycle-managed resource cleanup for middleware that hold file handles.NewFileLogMiddleware(middleware.go) — returns(Middleware, func() error)for file-logging middleware with proper fd cleanup. Pair withWithCleanupto avoid descriptor leaks in long-lived watchers.MiddlewareWriteFileLogdelegates to this but discards the closer (backward compat).- Hermetic
benchstat(flake.nix) —bench-diffnow uses abuildGoModule-builtbenchstatinstead ofgo run golang.org/x/perf/cmd/benchstat@latest(network, non-reproducible). nix run .#lint-tests(flake.nix) — dedicated app for explicit test-file linting (--testsflag).examples-buildnix check (flake.nix) —examples/added to the sourcefileset.unionsand a new check that compilesgo build ./examples/....- Commitlint CI gate (
.github/workflows/commitlint.yml) — validates PR commit subjects followtype(scope?): subjectconventional-commit format with 72-char limit. - Release-please workflow (
.github/workflows/release-please.yml) — opens release PRs from conventional commits, automating CHANGELOG + version bumps. Go-aware withvtag prefix. - Docs-consistency CI gate (
.github/workflows/docs-consistency.yml) — checks that deprecation claims don’t drift betweenREADME.mdandAPI_STABILITY.md. - Benchmark baseline workflow (
flake.nix) —nix run .#bench-baselinecaptures a clean (no-race,-count=6,-run=^$) baseline to the gitignoredbench-baseline.txt;nix run .#bench-diffruns fresh benchmarks throughbenchstatagainst it for one-command regression checks. Pairs with the ROADMAP “benchmark freshness CI” idea. GOTMPDIRin devShell (flake.nix) — the default devShell redirects Go’s temp dir to a disk-backed${XDG_CACHE_HOME:-$HOME/.cache}/go-filewatcher/gotmp, preventing tmpfs/tmpexhaustion during long sessions.- gocritic
exitAfterDeferexclusion forexamples/(.golangci.yml) — defensive path+text exclusion solog.Fatalin examplemain()functions doesn’t trigger per-line nolint fights. - Middleware default-const usage guard (
middleware_test.go) —TestMiddlewareDefaultConsts_AllUsedis an AST-based test asserting every middleware default const is both declared inmiddleware.goand referenced, catching dead constants if defaulting is refactored away (and drift if the expected list falls out of sync). FilterAndshort-circuit regression test (filter_test.go) —TestFilterAndShortCircuitsOnFirstFalselocks in the lazy evaluation contract.- Default-guard convention docs (
AGENTS.md) — a worked-example “Default-guard convention” subsection documenting the shared-vs-uniqueresolve*Defaultsdecision. WatchChangescontract sketch (docs/research/watchchanges-contract.md) — design for an idempotent reconcile-to-target-state sync API for sync/backup workflows.- Semantic-release / conventional-commits evaluation (
docs/research/semantic-release-evaluation.md) — recommendsrelease-please+ a commit-subject lint gate oversemantic-releasefor a Go module.
Changed
Section titled “Changed”nix run .#ci/.#fmt/.#tidyrun from caller CWD (flake.nix) — these write-modifying apps no longercd "${self}"into the read-only nix store; they operate on the working directory directly. Documented as “invoke from repo root.”MiddlewareDeduplicatecleanup trigger (middleware.go) — replacedlen(seen)%100 == 0with an event counter (eventCount%dedupeCleanupInterval), eliminating the quirk where a map hovering at a multiple of 100 triggered cleanup every event.- Self-heal respects permanent failures (
watcher_selfheal.go) —attemptSelfHealnow checksIsPermanent()and abandons paths that can never recover (deleted directory, wrong entry type) instead of retrying them forever. Retry budget is now spent only on genuinely transient failures. - Go toolchain bumped from 1.26.4 to 1.26.5
- Middleware default-guard consistency (
middleware.go) — every middleware default is now a named const (defaultThrottleEventsextracted; zero magic literals remain in default-guard code). Shared defaulting (2+ functions) goes through aresolve*Defaultshelper; single-function defaulting uses an inline guard with a named const. - Debouncer
Stoplifecycle consolidated (debouncer.go) —baseDebouncer.stop(cleanup)centralizes the lock → markStopped → cleanup → unlock → wait envelope shared byDebouncer.Stop()andGlobalDebouncer.Stop(). - Negative-duration panics unified (
options.go) —requireNonNegativeDuration(optionName, delay)is now used by bothWithDebounceandWithPerPathDebounce.
- README Prometheus snippet (
README.md) — replaced the broken polling-loop pattern (used fakeprometheus.ExemplarAddertype that would panic at runtime) with a correctprometheus.Collectorwrapper implementation (Describe + Collect usingMustNewConstMetric). Prometheus now scrapes on its own schedule — no polling loop needed. - README OTel snippet (
README.md) — fixed incorrect API references:trace.Attribute→attribute.KeyValue,trace.StringAttribute→attribute.String,trace.StatusError/trace.StatusOk→codes.Error/codes.Ok,stdouttracer.New()→ properstdouttrace.New()+sdktrace.NewTracerProvidersetup. Verified against the actual OTel SDK API (go.opentelemetry.io/otel/{attribute,codes,trace}+exporters/stdout/stdouttrace). MiddlewareWriteFileLogfd leak (middleware.go) — file handle was opened lazily but never closed. AddedNewFileLogMiddlewarereturning a closer, plusWithCleanupoption to register it with the watcher lifecycle.MiddlewareWriteFileLogdelegates toNewFileLogMiddleware(backward compat).bench-baseline.txtslog pollution (flake.nix) — benchmarks now use-run=^$to skip test functions (whose watcher error output polluted the baseline file). The reference file is now clean forbenchstat.- 4 broken
BenchmarkEmitEvent_*benchmarks (benchmark_test.go) — deadlocked because the size-1 event channel was never drained; the second emit blocked forever on a zero-valueWatcher(nildonechannel, uncancellable context). Extracted a sharedbenchmarkEmitEventhelper that uses a non-blocking drain, valid for both direct-emit and debounced paths. - 2 flaky tests hardened (
watcher_test.go,testing_helpers_test.go) —TestWatcher_Stats_MetricsandTestWatcher_Watch_WithMiddlewarerelied on fixedtime.Sleepto wait for async fsnotify delivery and counter propagation. Replaced with a newwaitForConditionpolling helper that waits deterministically until the assertion holds (or times out), eliminating the race that caused intermittent failures.
Deprecated
Section titled “Deprecated”WithOnError(options.go) — redundant convenience wrapper aroundWithErrorHandlerthat discards theErrorContext. Deprecated in favor ofWithErrorHandler; seeAPI_STABILITY.md.MiddlewareRateLimit(middleware.go) — strict special case ofMiddlewareThrottle(MiddlewareRateLimit(n)==MiddlewareThrottle(n, n)). Deprecated in favor ofMiddlewareThrottle; seeAPI_STABILITY.md.
Internal
Section titled “Internal”addAttemptCountwired up (error_simulation_test.go,fake_backend_test.go) — the previously-deadfakeBackend.addAttemptCountmethod is now used in self-heal and specific-path-failure tests to verify retry counts and per-path attempt tracking.- Deprecation docs published (
README.md,MIGRATION.md,api-reference.mdx) —WithOnErrorandMiddlewareRateLimitnow have visual deprecation callouts in README tables, a v2.3→v3 migration section in MIGRATION.md, and Starlight deprecation badges in the website API reference. WithCleanup+NewFileLogMiddlewarein API_STABILITY.md — registered as Evolving APIs.- Backend abstraction for testability (
backend.go) — introducedwatchBackendinterface abstracting*fsnotify.Watcher, withfsnotifyBackendadapter for production and unexportedwithBackend()option for test injection.Watcher.fswatcherfield type changed from*fsnotify.WatchertowatchBackend. Enables deterministic error simulation without a real filesystem. - Error simulation test suite (
error_simulation_test.go,fake_backend_test.go) — 11 tests covering self-heal retry/abandon, error channel propagation, full pipeline event flow, closed-backend graceful shutdown, circuit breaker state machine, and error recovery strategy. Uses a fake backend that scripts Add failures (ENOSPC, permission denied, permanent), event injection, and error injection. MustWatchhelper (examples/demo/shared.go) — eliminates the lastart-dupl -t 1clone group in examples. All 4 examplemain()functions migrated;filter-generatedalso fixed a pre-existing resource leak (deferred Close ran before events were consumed).resolveBatchDefaultsunit test (middleware_test.go) — 6 table-driven cases closing the coverage gap on the third sharedresolve*helper.- Test-helper adoption —
newTestWatcher(the pre-existing but underusedNew + err-check + defer Closehelper) now has 96 call sites across 12 test files (grep -rc newTestWatcher *_test.go); inlineNew(calls in tests were reduced to the deliberately-retained set (benchmarks,Example*functions, error-path and lifecycle tests). - Direct unit tests added for the shared helpers:
resolveRateLimitDefaults(6 table cases),resolveMaxFailures(3 table cases),resolveBatchDefaults(6 table cases),requireNonNegativeDuration(panic + zero/positive subtests). - Examples lint hygiene — extracted
debounceDelaynamed consts inexamples/basicandexamples/per-path-debounce(resolves pre-existingmndwarnings); fixedgocritic exitAfterDeferand stalenolintlintdirectives inexamples/middlewareandexamples/filter-generated. .editorconfigadded for consistent editor settings across platforms.art-duplclone groups driven to zero at all thresholds (-t 1through-t 5); the previous surviving groups at-t 1were eliminated by theMustWatchhelper inexamples/demo.- Ecosystem integration (dogfooding) — go-filewatcher integrated into three real consumer projects:
dynamic-markdown-site,auto-deduplicate, and Cyberdom. Validates the public API surface against production usage patterns. docs/DOMAIN_LANGUAGE.mdrefreshed — added missing terms:FilterWithMeta,MatchResult,ContentHash,ErrorCategory,CircuitState(CircuitClosed/CircuitOpen/CircuitHalfOpen),Handler,Circuit Breaker,Error Category.- Docs-consistency exemption list shrunk 36→14 (
docs-consistency.yml) — documented 22 previously-exempted exported symbols in FEATURES.md (error taxonomy, metrics types, filter variants, debouncer constructors). Remaining exemptions are phantom-type helpers (11) + 2 filter gaps + 1 deprecated. wrapHandlerWithNilReturnarchitectural constraint documented (watcher_internal.go) — added doc comment explaining why middleware errors are absorbed (prevents cascade) and the implication for error-aware middleware (must be innermost layer to observe handler failures).
[2.2.1] - 2026-07-24
Section titled “[2.2.1] - 2026-07-24”- Nix flake support —
flake.nixwith devShell, quality-gate apps (nix run .#check,.#lint,.#test,.#ci), and reproducible vendored builds (proxyVendor,vendorHash) .golangci.yml— strict linting configuration with 50+ linters (exhaustruct, wrapcheck, paralleltest, gci, etc.).gitattributesfor consistent line-ending handling across platformsFEATURES.md— honest feature inventory with status indicators (DONE, PARTIALLY DONE, PLANNED, WORTH CONSIDERING)ROADMAP.md— long-term direction, raw ideas, and non-goals- Public documentation website (
website/) — Astro + Starlight site deployed to Firebase Hosting atfilewatcher.lars.software, with 17 components, full docs content, PWA manifest, and dark/light theming docs/DOMAIN_LANGUAGE.md— rebuilt from scratch with 20+ domain terms grounded in actual code (Watcher, Event, Op, Filter, Middleware, Debouncer, Watch Budget, Self-healing, Polling Mode, etc.)CODE_OF_CONDUCT.md— contributor community guidelines- Competitive analysis research:
docs/research/go-filewatcher-vs-ro-fsnotify.mdanddocs/research/adopting-samber-ro-pro-contra.md AddRecursiveerror messages now include themaxDepthparameter for easier debugging
Changed
Section titled “Changed”- Go toolchain bumped from 1.26.3 to 1.26.4
README.mdrewritten as a marketing-focused landing page with feature comparison table, “How it works” guide, and link to the documentation websiteDefaultIgnoreDirsCopy()now usesslices.Cloneinstead of a manual slice copywithResolvedPathhelper consolidates the lock → closed-check →filepath.Abs→ error-wrapping prologue shared byAdd,AddRecursive, andRemove— eliminates copy-pasted boilerplaterunExampleWatcherhelper deduplicates watcher setup across ~20 godoc examples inexample_test.godemo.Runhelper deduplicates context setup across example programs inexamples/- Import ordering in
watcher.gonormalized via goimports AGENTS.mdrestructured: removed version-stamped gotchas, added pointers to companion docs (FEATURES/ROADMAP/TODO_LIST), added website section and file organization forfilter_gogen.goandphantom_types.goTODO_LIST.mdrebuilt: removed 140+ completed historical items, kept only actionable short/mid-term work with clear priorities- CHANGELOG backfilled with three missing historical releases: v0.2.1, v0.2.2, v0.3.0
- CI: added
GOEXPERIMENT=jsonv2, pinnedgolangci-lintto v2.12.2, aligned coverage threshold from 90% to 85% (actual: 85.8%) makezerolint compliance: switched from pre-allocated indexing tomake([]T, 0, N)+appendpattern- Expanded
.gitignorewith JS/TS build artifacts and cache directories
Dependencies
Section titled “Dependencies”github.com/LarsArtmann/gogenfilter/v3updated from v3.1.0 to v3.2.0 — removed unused transitive dependencies (pprof, ginkgo, gomega, x/net)
[2.2.0] - 2026-06-03
Section titled “[2.2.0] - 2026-06-03”WithGitignore(enabled bool)option —.gitignore-aware directory walking, enabled by defaultWithExcludePaths(paths...)option — exclude absolute paths during walk (prefix matching, skips subdirectories)WithMaxWatches(n int)option — override inotify watch budget; auto-detected from/proc/sys/fs/inotify/max_user_watcheson LinuxWithContentHashing()option — SHA-256 content hash inEvent.Hashfield (opt-in, capped at 10 MiB)WithSelfHeal(interval)option — self-healing watcher that auto-retries failed watch paths at configurable intervalsFilterGitignore(repoRoot string)filter — match files against.gitignorepatterns from a repository rootFilterWithMetatype,MatchResultstruct,FilterFromWithMeta(),FilterWithMetaAnd/Or/Not(),WithMeta()wrapper — filter functions that return match metadataMiddlewareExponentialBackoff()— configurable exponential backoff for event processing with initial/max intervalsPrometheusCollector— zero-dependency Prometheus collector withStatsFunc,CounterMetric,GaugeMetricinterfacesOTelMiddleware— zero-dependency OpenTelemetry tracing middleware withOTelSpaninterfaceEvent.Hashfield for content hash metadataWatcher.Reset()method — clears runtime state while preserving configuration (filters, middleware, debounce, options)Remove()now cleans up all subdirectory watches under the given path to prevent watch leaksStats.WatchErrors— tracks how many paths failed to add during walkStats.WatchLimitandStats.WatchBudgetUsed— track inotify budget usage- Batched watch registration — directories collected during walk and added in batches of 1000 with
runtime.Gosched()between batches - Graceful ENOSPC handling —
fswatcher.Add()errors no longer abort the entire walk; errors are logged and walking continues in degraded mode - 7 new godoc examples:
ExampleWatcher_Add,ExampleWatcher_Errors,ExampleWithErrorHandler,ExampleWithDebug,ExampleFilterMinSize,ExampleOp,ExampleOp_MarshalJSON - CI:
examples-buildjob and benchmark artifact upload for regression detection
Changed
Section titled “Changed”MiddlewareRateLimitnow delegates toMiddlewareThrottleinternally, eliminating duplicate rate-limiting logicaddPathunified into a single code path — eliminatedwalkDirFuncduplication for recursive and non-recursive walks- Generic
makeSetFilter[T]replaces duplicate extension and operation filter factories - Shared
hashFilefunction consolidates SHA-256 logic from filter and self-heal modules
.gitignorematching now correctly handles ancestor-only patterns and avoids doubleos.StatcallsmaxWatchesbudget check applied to all directaddPathcalls, not just walked directories- Gitignore, exclude paths, watch budget, and ENOSPC handling now apply to
addPathWithDepthfor nested directories - Duplicate root paths no longer appear in
WatchList()after recursive walks
Dependencies
Section titled “Dependencies”github.com/sabhiram/go-gitignoreadded for.gitignorepattern matching (zero transitive deps)github.com/LarsArtmann/gogenfilterupdated to v3.0.3
[2.1.0] - 2026-06-01
Section titled “[2.1.0] - 2026-06-01”Event.SizeandEvent.ModTimefields for file metadata in eventsWithPolling()andWithPollInterval()options for NFS/FUSE filesystem supportWithDebug()option with structuredslogdebug logging throughout the pipelineErrorCodetyped constants for programmatic error matching (WATCHER_CLOSED,PATH_NOT_FOUND, etc.)WatcherError.Stack— auto-captureddebug.Stack()traces for debuggingFilterContentHash()filter for SHA-256 content-change detectionMiddlewareCircuitBreaker()— fault tolerance with closed/open/half-open statesMiddlewareThrottle()— fixed-window rate limitingMiddlewareWriteFileLog()— audit trail to filesystemMiddlewareErrorSanitization()— safe error message scrubbing preserving error chainsMiddlewareErrorRateLimit()— per-error-type rate limitingMiddlewareErrorRecovery()— recoverable error handling with custom strategiesMiddlewareErrorCorrelation()— request tracing across handler chainsMiddlewareErrorBatch()— batch error flushing for analyticsWithFollowSymlinks()option for symbolic link traversal during directory walking- Fuzz tests for
ParseFamily,Classify, and error formatting .goreleaser.ymlconfiguration for cross-platform release artifactsAPI_STABILITY.md— public API stability policy and guaranteesTroubleshooting.md— common issues and resolutionsCODE_OF_CONDUCT.md— contributor code of conductdocs/research/directory for architecture and adoption research
Changed
Section titled “Changed”- Go version bumped to 1.26.3 (was 1.26.2)
Event.ModTimeJSON tag changed fromomitemptytoomitzerofor correct zero-time handlingEventLogValuenow includessizeandmodTimefields- Refactored
copyWatchList()helper, eliminating 3× lock+copy duplication in path management - Refactored
ErrorCategory.String()to inline string constants, removing redundantcategoryStrvariables - Streamlined nolint directives and extracted magic strings to named constants across the codebase
- Project documentation and configuration files reorganized
WatchOnce()double%wformatting caused silent error droppingMiddlewareErrorSanitizationnow preserves error chains via%winstead of discarding themcategorizeError()was missing several sentinel errors in its classification mappingOpStringreceiver variable shadowed theospackage importEvent.ModTimeJSON serialization now correctly distinguishes zero values- CI: aligned
golangci-lint-actionto v7 - CI: added
go vetandgo fmtchecks to the lint workflow
Deprecated
Section titled “Deprecated”WithWatchedIgnoreDirs()— superseded byWithIgnoreDirs(). Will be removed in v3.0.0.
[2.0.0] - 2026-05-23
Section titled “[2.0.0] - 2026-05-23”DefaultIgnoreDirsCopy()function for safe access without mutation risk- Debounce option validation: panics on negative durations
Errors() <-chan errormethod for channel-based error consumptionIsWatching()andIsClosed()state inspection methodsWithLazyIsDir()option to skipos.Statcalls for performanceWithOnAdd()callback option for path trackingWithOnError()simplified error callback optionFilterMaxSize(),FilterMinAge(),FilterModifiedSince()filtersMiddlewareDeduplicate(),MiddlewareBatch(),MiddlewareSlidingWindowRateLimit()FilterGeneratedCode(),FilterGeneratedCodeFull()via gogenfilter integration- Compile-time phantom types for
EventPath,RootPath,DebounceKey,OpString Event.GetPath()returning phantom-typedEventPathslog.LogValueronEventfor structured logging- 15 new tests covering rename events, multi-directory init, concurrent ops, state transitions
Changed
Section titled “Changed”- BREAKING: Relicensed from Proprietary to MIT
- BREAKING: Module path changed to
github.com/larsartmann/go-filewatcher/v2 - Replaced hand-rolled
Op.MarshalJSONwithjson.Marshalfor robustness - Modernized
errors.Asto Go 1.26AsTypepattern testing_helpers.gorenamed totesting_helpers_test.go(no longer ships to consumers)flake.nixGo version aligned to 1.26 (was 1.24)FilterExcludePathsno longer callsfilepath.Absper eventWithBuffer(0)now allowed with documented caveat
Add()no longer double-appends toWatchList()in recursive modeMiddlewareBatchtimer-triggered flush errors now logged viaslog.Errorinstead of silently droppedhandleNewDirectorynow propagatesaddPatherrors to the error handlerMiddlewareSlidingWindowRateLimituses in-place slice compaction instead of per-event allocation
Removed
Section titled “Removed”- 306 lines of test-only code from production binary
[0.3.0] - 2026-05-05
Section titled “[0.3.0] - 2026-05-05”Version bump — same commit as v0.2.2. No additional code changes.
[0.2.2] - 2026-05-05
Section titled “[0.2.2] - 2026-05-05”- Migrated gogenfilter import to
/v3module path
[0.2.1] - 2026-05-04
Section titled “[0.2.1] - 2026-05-04”WatchOnce(ctx)— one-shot mode that returns the first event and closesMiddlewareThrottle(maxEvents, burst)— token-bucket rate limiting viagolang.org/x/time/rateFilterIgnoreGlobs(patterns...)andWithIgnorePatterns(pats...)option- MIT license (relicensed from Proprietary)
- Benchmark CI job and Dependabot configuration
- 15 new tests covering WatchOnce, MiddlewareThrottle, FilterIgnoreGlobs, WithIgnorePatterns
Changed
Section titled “Changed”- Migrated gogenfilter from v0.2.0 to v3.x
- Simplified phantom types from go-branded-id to plain named types
- Replaced custom rate-limit implementations with
golang.org/x/time/rate - Pinned nixpkgs to nixos-unstable instead of channel default
WatchOncenil-error wrapping in cancellation path- Double-append bug, error swallowing, toolchain, and test pollution issues
[0.2.0] - 2026-04-23
Section titled “[0.2.0] - 2026-04-23”go-branded-idintegration for compile-time phantom type safetyFilterGeneratedCode()andFilterGeneratedCodeFull()via gogenfilter v0.2.0OpString,LogSubstring,TempDir,DebounceKey,RootPathphantom types- Extracted shared test helper functions for DRY
- Benchmark suite migrated to
b.Loop()pattern
Changed
Section titled “Changed”- Migrated to gogenfilter v0.2.0 API
- Updated flake.lock to nixpkgs eb3b085
- Data race between
Close()andbuildEmitFunc - Data race between
Close()and debouncer callbacks - fsnotify assertion tests tolerant of duplicate events
[0.1.0] - 2026-04-04
Section titled “[0.1.0] - 2026-04-04”- Core watcher:
New(),Watch(ctx)→<-chan Event,Add(),Remove(),WatchList(),Stats(),Close() - 14 functional options: debounce, per-path debounce, filter, extensions, ignore dirs, ignore hidden, recursive, middleware, error handler, skip dot dirs, buffer, on add, on error, lazy is dir
- 13 composable filters: Extensions, IgnoreExtensions, IgnoreDirs, ExcludePaths, IgnoreHidden, Operations, NotOperations, Glob, Regex, MinSize, MaxSize, MinAge, ModifiedSince
- Filter combinators:
FilterAnd,FilterOr,FilterNot - 10 middleware: Logging, Recovery, Filter, OnError, RateLimit, SlidingWindowRateLimit, Metrics, Deduplicate, Batch, WriteFileLog
- Per-key
DebouncerandGlobalDebouncerwith Flush/Pending/Stop - 10 sentinel errors with structured
WatcherError(transient/permanent categorization) Errors() <-chan errorfor channel-based error consumptionIsWatching()andIsClosed()state inspection- Channel-based event streaming with context cancellation
- Automatic recursive directory watching with dynamic new-dir detection
MiddlewareLoggingaccepts*slog.Loggerfor structured loggingslog.LogValueronEventtype- JSON marshaling for
OpandEventtypes - Benchmarks for creation, filters, middleware, debounce, full pipeline
- GitHub Actions CI (test with race + 90% threshold, lint with 90+ rules)
- Nix flake dev shell for 4 platforms
- Comprehensive documentation: README, ARCHITECTURE.md, MIGRATION.md, examples
Changed
Section titled “Changed”- Replaced
cockroachdb/errorswith stdlib (eliminated 39 transitive dependencies) - Split
watcher.gointowatcher.go,watcher_internal.go,watcher_walk.go
Removed
Section titled “Removed”cockroachdb/errorsdependency- Dead artifacts:
report/jscpd-report.json, emptypkg/directory