From a45f8b5ce490954c2ea816036c7998471fa4a679 Mon Sep 17 00:00:00 2001 From: Johnny Miller <163300+millerjp@users.noreply.github.com> Date: Tue, 21 Apr 2026 06:31:47 +0200 Subject: [PATCH 1/3] docs: add README, CHANGELOG, NOTICE, SECURITY, and LLM docs bundle (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 user-facing documentation: * README.md — rewritten from the 1-line placeholder. Shop-front hero, TOC, Quick Start (compiles via TestReadmeQuickStart_Compiles), full API reference table, thread-safety notes, performance section linking bench.txt, when-to-use guidance, AI assistant section, security + attribution + licence. * CHANGELOG.md — Keep-a-Changelog format; v1.0.0 entry enumerates every fork change including the Items→Values breaking rename from #12, Swap+Clear from #13, and CompareAndSwap/Delete from #14. Upstream credited as Apache 2.0 already; no relicensing. * NOTICE — AxonOps Limited + Robert Gooding / rgooding/go-syncmap upstream attribution. * SECURITY.md — supported-versions table, syncmap-specific threat model (concurrency primitive), private-reporting contact oss@axonops.com. * llms.txt — concise AI-assistant summary (1115 words, under the 2250 budget). Includes the attribution-guard rule so any assistant reading it knows not to sign commits with AI tokens. * llms-full.txt — generated concatenation of llms.txt, README, doc.go package comment, SECURITY, CHANGELOG, and go doc -all. * scripts/gen-llms-full.sh — adapted from mask; source list tailored to the files syncmap has today. * .markdownlint-cli2.yaml — mask config with syncmap-specific globs (no docs/rules.md / docs/extending.md). * Makefile — new llms-full and llms-full-check targets; makefile-targets-guard expected list extended. * .github/workflows/ci.yml — markdownlint and llms-full-up-to-date jobs enabled. * documentation_test.go — AI-friendliness and governance tests adapted from mask: llms.txt budget, llms-full.txt section headers, llms-full.txt byte-equality via regenerated script, required Example functions present, every exported symbol has real (non-mechanical, multi-line) godoc, README Quick Start compiles and produces the documented output, NOTICE / SECURITY / CHANGELOG governance presence checks. Remaining Phase 5 content (CONTRIBUTING, CLA, CODE_OF_CONDUCT, CONTRIBUTORS, cla.yml/contributors.yml workflows) is tracked by #18. Coverage remains 100%. --- .github/workflows/ci.yml | 26 ++ .markdownlint-cli2.yaml | 50 +++ CHANGELOG.md | 58 ++++ Makefile | 16 + NOTICE | 23 ++ README.md | 180 +++++++++- SECURITY.md | 59 ++++ documentation_test.go | 368 ++++++++++++++++++++ llms-full.txt | 708 +++++++++++++++++++++++++++++++++++++++ llms.txt | 123 +++++++ scripts/gen-llms-full.sh | 85 +++++ 11 files changed, 1694 insertions(+), 2 deletions(-) create mode 100644 .markdownlint-cli2.yaml create mode 100644 CHANGELOG.md create mode 100644 NOTICE create mode 100644 SECURITY.md create mode 100644 documentation_test.go create mode 100644 llms-full.txt create mode 100644 llms.txt create mode 100755 scripts/gen-llms-full.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e7664c..0d7010c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,8 @@ jobs: tidy-check security release-check + llms-full + llms-full-check clean help ) @@ -152,6 +154,30 @@ jobs: exit "$failed" + llms-full-up-to-date: + name: llms-full.txt is up to date + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + - uses: actions/setup-go@v6.4.0 + with: + go-version: "1.26" + cache: true + - name: Regenerate and diff + run: make llms-full-check + + markdownlint: + name: Markdown lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + - uses: DavidAnson/markdownlint-cli2-action@v21.0.0 + with: + globs: | + README.md + SECURITY.md + CHANGELOG.md + bdd-strict-mode-guard: name: BDD strict mode guard runs-on: ubuntu-latest diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml new file mode 100644 index 0000000..2016acf --- /dev/null +++ b/.markdownlint-cli2.yaml @@ -0,0 +1,50 @@ +# markdownlint-cli2 configuration for the docs tracked by this repo. +# Rules are tuned for this project's prose style — see +# https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md. + +globs: + - "README.md" + - "CONTRIBUTING.md" + - "SECURITY.md" + - "CHANGELOG.md" + - "CLA.md" + - "CODE_OF_CONDUCT.md" + - "CONTRIBUTORS.md" + +config: + default: true + + # MD013 — long lines are the norm for prose and for inline code/table cells. + MD013: false + + # MD033 — README uses
for the hero block and + #
for collapsible sections. + MD033: false + + # MD041 — the README opens with the centred hero block (HTML + image), + # not an H1. The H1 appears inside the hero. + MD041: false + + # MD024 — sibling headings may repeat between unrelated sections. + MD024: + siblings_only: true + + # MD036 — emphasis-as-heading is used for the bold tagline in the hero. + MD036: false + + # MD034 — bare URLs are acceptable in the generator output bundle; + # prose links use markdown syntax. + MD034: false + + # MD010 — Go code blocks in the README use tabs (the language's + # canonical indentation), so exclude fenced code blocks from the + # hard-tab check. + MD010: + code_blocks: false + + # MD060 — table pipe padding style is not enforced. + MD060: false + + # MD023 — H1/H2 inside the hero are intentionally indented so GitHub + # renders them centred inside the
block. + MD023: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..813a38b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,58 @@ +# Changelog + +All notable changes to `github.com/axonops/syncmap` are documented in this file. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +No unreleased changes. + +## Upgrading + +From `v1.0.0` onwards `syncmap` follows the standard Go semantic-versioning +compatibility promise: breaking changes to the public API only in a new +major version. Minor and patch releases are always backwards-compatible +for the API surface documented on [pkg.go.dev](https://pkg.go.dev/github.com/axonops/syncmap). +Pin a specific tag in your `go.mod`, review the release notes for the +target version, and run your test suite with `-race` against the new +version before rolling to production. + +## [1.0.0] — 2026-04-21 + +Initial AxonOps release. Forked from [`github.com/rgooding/go-syncmap`](https://github.com/rgooding/go-syncmap) by Robert Gooding and rebuilt to AxonOps library standards — new module path, expanded API, comprehensive tests, CI/CD, and documentation. + +### Added + +- **Core API** mirroring [`sync.Map`](https://pkg.go.dev/sync#Map): `Load`, `Store`, `LoadOrStore`, `LoadAndDelete`, `Delete`, `Range` — each generic over `K comparable, V any`, returning the typed zero value of `V` on miss so callers never deal with untyped `any`. +- **Extension methods** beyond `sync.Map`: `Len`, `Map` (snapshot as a plain Go map), `Keys`, `Values` — each documented as `O(n)` and a point-in-time approximation under concurrent mutation. +- **`Swap` method** wrapping `sync.Map.Swap` (Go 1.20) with the same typed-zero-on-miss guard as `LoadAndDelete`. +- **`Clear` method** wrapping `sync.Map.Clear` (Go 1.23). +- **`CompareAndSwap` and `CompareAndDelete`** as package-level generic functions with a tighter `[K, V comparable]` constraint, so non-comparable value types (slice, map, func) are rejected at compile time rather than panicking at runtime inside `sync.Map`. The `SyncMap[K, V any]` type signature is unchanged. +- **Package documentation** (`doc.go`) covering relationship to `sync.Map`, when to use `SyncMap` vs `sync.Map` vs `map + sync.RWMutex`, thread safety, zero-value usability, and a runnable Quick Start. +- **Unit tests** — external black-box package (`syncmap_test`) using `testify` and [`go.uber.org/goleak`](https://pkg.go.dev/go.uber.org/goleak). Every test runs under `-race` with `t.Parallel()`. Line coverage is **100%** of the library package. +- **Runnable godoc examples** covering every public symbol, each ending with a deterministic `// Output:` block. +- **Benchmarks** for every public method, plus a concurrent 90/10 read-write pattern and overhead pairs comparing the generic wrapper against raw `sync.Map`. The committed `bench.txt` baseline is the reference the CI `benchstat-regression-guard` job diffs against. +- **BDD suite** — [`godog`](https://pkg.go.dev/github.com/cucumber/godog) feature files under `tests/bdd/` exercising every public symbol plus a concurrent Store scenario. Runs under strict mode enforced by a CI guard. +- **Fuzz targets** — `FuzzLoadStore` (round-trip invariant) and `FuzzConcurrent` (4 goroutines over random op sequences, race-clean). +- **CI** (`.github/workflows/ci.yml`): format check, vet, golangci-lint, unit + BDD tests, 95% coverage threshold, module tidy, govulncheck, cross-platform builds (`linux/amd64`, `darwin/arm64`, `windows/amd64`), benchstat regression guard, BDD strict-mode guard, Apache-header guard, no-local-paths guard, no-AI-attribution guard, Makefile-targets guard, markdown lint, and `llms-full.txt` drift guard. +- **Release workflow** (`.github/workflows/release.yml`): `workflow_dispatch` only; verifies, tags, publishes via GoReleaser, warms the Go module proxy. Local tag creation is forbidden. +- **Dependabot** configuration with weekly updates and auto-merge for patch-level test dependencies. +- **LLM documentation bundle**: `llms.txt` (concise summary) and `llms-full.txt` (concatenated corpus) for AI-assistant ingestion, with a CI guard that fails the build on drift. +- `LICENSE` (Apache 2.0, preserved from upstream), `NOTICE` (crediting Robert Gooding as the upstream author), `SECURITY.md`. + +### Changed + +- Module path: `github.com/rgooding/go-syncmap` → `github.com/axonops/syncmap`. +- Minimum Go toolchain raised to **1.26**. + +### Breaking + +- Renamed the `Items()` method on `SyncMap` to `Values()` to match Go stdlib convention (`maps.Values`, Go 1.23). No deprecation shim — the rename lands pre-v1.0 under the new module path. + +### Attribution + +This release is a fork of [`github.com/rgooding/go-syncmap`](https://github.com/rgooding/go-syncmap) by Robert Gooding, which is distributed under Apache 2.0; this fork continues under the same licence. The original upstream copyright is preserved in git history and credited in `NOTICE`. + +[Unreleased]: https://github.com/axonops/syncmap/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/axonops/syncmap/releases/tag/v1.0.0 diff --git a/Makefile b/Makefile index a4c10b4..9f30a79 100644 --- a/Makefile +++ b/Makefile @@ -99,6 +99,22 @@ security: ## Run govulncheck release-check: ## Validate GoReleaser config without releasing $(GORELEASER) check +.PHONY: llms-full +llms-full: ## Regenerate llms-full.txt from its canonical sources + @./scripts/gen-llms-full.sh + +.PHONY: llms-full-check +llms-full-check: ## Fail if llms-full.txt is out of date + @cp llms-full.txt llms-full.txt.bak + @trap 'mv -f llms-full.txt.bak llms-full.txt 2>/dev/null || true' EXIT; \ + ./scripts/gen-llms-full.sh >/dev/null; \ + if ! diff -q llms-full.txt llms-full.txt.bak >/dev/null; then \ + echo "llms-full.txt drift — run 'make llms-full' and commit the result"; \ + exit 1; \ + fi; \ + rm -f llms-full.txt.bak; \ + trap - EXIT + .PHONY: clean clean: ## Remove generated test and coverage artefacts $(GO) clean -testcache diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..b23ef51 --- /dev/null +++ b/NOTICE @@ -0,0 +1,23 @@ +syncmap +Copyright 2026 AxonOps Limited. + +This product includes software developed at +AxonOps Limited (https://axonops.com/). + +This product is a fork of github.com/rgooding/go-syncmap by +Robert Gooding, which is distributed under the Apache License, +Version 2.0. This fork continues under the same licence. The +original upstream copyright notice is preserved in this +repository's git history. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index bf1483e..f535a6f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,178 @@ -# go-syncmap -Wrapper around sync.Map using generics to give a cleaner interface +
+ + # syncmap + + **Type-safe generic wrapper around Go's `sync.Map` — zero dependencies, zero assertion cost at the call site.** + + [![CI](https://github.com/axonops/syncmap/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/axonops/syncmap/actions/workflows/ci.yml) + [![Go Reference](https://pkg.go.dev/badge/github.com/axonops/syncmap.svg)](https://pkg.go.dev/github.com/axonops/syncmap) + [![Go Report Card](https://goreportcard.com/badge/github.com/axonops/syncmap)](https://goreportcard.com/report/github.com/axonops/syncmap) + [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE) + ![Status](https://img.shields.io/badge/status-pre--release-orange) + + [🚀 Quick Start](#-quick-start) | [📖 API](#-api-reference) | [🧵 Thread Safety](#-thread-safety) | [⚡ Performance](#-performance) | [🤖 For AI assistants](#-for-ai-assistants) + +
+ +--- + +**Table of contents** + +- [✅ Status](#-status) +- [🔍 Overview](#-overview) +- [✨ Why `syncmap`?](#-why-syncmap) +- [🚀 Quick Start](#-quick-start) +- [📖 API Reference](#-api-reference) +- [🧵 Thread Safety](#-thread-safety) +- [⚡ Performance](#-performance) +- [🧭 When to use what](#-when-to-use-what) +- [🤖 For AI assistants](#-for-ai-assistants) +- [🤝 Contributing](#-contributing) +- [🔐 Security](#-security) +- [📜 Attribution](#-attribution) +- [📄 Licence](#-licence) + +--- + +## ✅ Status + +`syncmap` is **stable** from `v1.0.0` onwards and follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html): breaking changes to the public API only in a new major version. Pin a specific tag in your `go.mod` and review the [CHANGELOG](./CHANGELOG.md) on every upgrade. + +## 🔍 Overview + +`github.com/axonops/syncmap` is a thin, typed layer over Go's standard [`sync.Map`](https://pkg.go.dev/sync#Map). The standard `sync.Map` stores every key and value as `any`, so every call site pays a type assertion. `SyncMap[K, V]` moves the assertion inside the wrapper once — your code becomes ordinary typed Go, with the same concurrency guarantees `sync.Map` already provides and no additional allocations. + +```go +var m syncmap.SyncMap[string, int] +m.Store("hits", 1) +v, ok := m.Load("hits") +// v is an int, not interface{}. No `.(int)` at the call site. +``` + +## ✨ Why `syncmap`? + +- **Compile-time type safety.** `SyncMap[K comparable, V any]` is fully generic. `Load`, `Store`, `Range`, and friends accept and return your types directly. +- **Zero runtime dependencies.** Stdlib only. No goroutines spawned by the library. No transitive dependency CVEs to track. +- **Stdlib-parity semantics.** Every method maps one-to-one to a `sync.Map` method. `Load` on a missing key returns the typed zero value of `V` and `ok=false`, matching the stdlib contract. +- **The full modern API.** `Swap` (Go 1.20), `Clear` (Go 1.23), plus `CompareAndSwap` / `CompareAndDelete` as package-level generic functions that reject non-comparable `V` at **compile time** instead of the stdlib's runtime panic. +- **Convenience helpers** beyond `sync.Map`: `Len`, `Map`, `Keys`, `Values`. Each is documented as `O(n)` and a point-in-time approximation — no surprises. +- **Race-clean under scrutiny.** The suite runs with `-race`, `goleak.VerifyTestMain`, 100 % line coverage, 37 BDD scenarios, 2 fuzz targets, and a benchstat regression gate in CI. + +## 🚀 Quick Start + +```go +package main + +import ( + "fmt" + + "github.com/axonops/syncmap" +) + +func main() { + var m syncmap.SyncMap[string, int] + + m.Store("hits", 1) + m.Store("misses", 0) + + if v, ok := m.Load("hits"); ok { + fmt.Println("hits:", v) // hits: 1 + } + + // CompareAndSwap is a package-level function — V must be comparable. + if syncmap.CompareAndSwap(&m, "hits", 1, 2) { + fmt.Println("incremented") + } +} +``` + +Install: + +```bash +go get github.com/axonops/syncmap@latest +``` + +Requires Go 1.26 or later. + +## 📖 API Reference + +Complete godoc at [pkg.go.dev/github.com/axonops/syncmap](https://pkg.go.dev/github.com/axonops/syncmap). The public surface in full: + +| Symbol | Signature | Notes | +|---|---|---| +| `SyncMap[K, V]` | `type SyncMap[K comparable, V any] struct{…}` | Zero value is ready to use; do not copy after first use. | +| `Load` | `(key K) (value V, ok bool)` | Returns typed zero V on miss. | +| `Store` | `(key K, value V)` | Sets value for key. | +| `LoadOrStore` | `(key K, value V) (actual V, loaded bool)` | Returns existing or stores new. | +| `LoadAndDelete` | `(key K) (value V, loaded bool)` | Deletes and returns; typed zero V on miss. | +| `Delete` | `(key K)` | No-op if key absent. | +| `Swap` | `(key K, value V) (previous V, loaded bool)` | Go 1.20 semantics; typed zero V on miss. | +| `Clear` | `()` | Go 1.23 semantics; removes every entry. | +| `Range` | `(f func(K, V) bool)` | Not a consistent snapshot — see godoc. | +| `Len` | `() int` | **O(n)** — point-in-time approximation. | +| `Map` | `() map[K]V` | **O(n)** — snapshot copy; caller owns the result. | +| `Keys` | `() []K` | **O(n)** — order undefined. | +| `Values` | `() []V` | **O(n)** — order undefined; not correlated with `Keys`. | +| `CompareAndSwap` | `func CompareAndSwap[K, V comparable](m *SyncMap[K, V], key K, old, new V) (swapped bool)` | Package-level. `V` must be comparable. | +| `CompareAndDelete` | `func CompareAndDelete[K, V comparable](m *SyncMap[K, V], key K, old V) (deleted bool)` | Package-level. `V` must be comparable. | + +Every symbol has a runnable godoc `Example` in [`example_test.go`](./example_test.go) — open any one of them on pkg.go.dev to see the exact usage pattern. + +## 🧵 Thread Safety + +All methods on `SyncMap` are safe for concurrent use by multiple goroutines **without additional locking**. This guarantee is inherited directly from `sync.Map`. + +A few specifics worth calling out: + +- `Range` does **not** correspond to a consistent snapshot. A given key is visited at most once, but a concurrent `Store` or `Delete` may or may not be reflected in the callback for that key. Identical to the stdlib `sync.Map.Range` contract. +- `Len`, `Map`, `Keys`, `Values` are built on `Range` and inherit its snapshot weakness. Treat the results as approximations, not atomic views. If you need a consistent multi-key view, serialise through your own lock. +- `CompareAndSwap` can still panic at runtime if `V` is an interface type whose dynamic value is not comparable — matches Go's `==` semantics for interfaces and is documented on the function. + +## ⚡ Performance + +Overhead against raw `sync.Map` is effectively zero. The committed [`bench.txt`](./bench.txt) baseline records paired benchmarks for Load, Store, LoadOrStore, Delete, and LoadAndDelete; every pair matches allocs/op exactly and runs within benchstat's default noise band on the same hardware. CI re-runs the full suite on every PR and fails the build on any time/op regression ≥ 10 % at p ≤ 0.05 or any positive allocs/op delta. + +Run locally: + +```bash +make bench # one-shot benchmarks +make bench-regression # compare this tree against bench.txt +``` + +## 🧭 When to use what + +`sync.Map` (and therefore `SyncMap`) is the right tool for specific workloads. It is **not** a general-purpose concurrent map. + +| Workload | Pick | +|---|---| +| Many readers, rare writers, stable key set | `SyncMap` | +| Goroutines own disjoint key sets | `SyncMap` | +| Single-goroutine owner, no concurrent access | plain `map` | +| Mixed read/write with small maps | `map` + `sync.RWMutex` | +| Mixed read/write with bounded high churn | `map` + `sync.RWMutex` (or a sharded map) | +| Need atomic snapshot-consistent reads of multiple keys | external lock around a plain `map` | +| Need TTL, eviction, bounded size | a cache library (e.g. `ristretto`, `otter`) | + +`SyncMap` inherits every performance characteristic of `sync.Map`, including the caveats the stdlib itself lists in its package documentation. Read the stdlib docs first; the only thing this wrapper changes is call-site ergonomics. + +## 🤖 For AI assistants + +This repository ships [`llms.txt`](./llms.txt) (concise summary) and [`llms-full.txt`](./llms-full.txt) (full concatenated corpus) at the repo root. Ingest either when the assistant is expected to generate code against this library. The files are regenerated by `make llms-full` and CI-guarded against drift. + +**Attribution rule.** Commit messages, PR descriptions, code comments, and any other tracked artefact **must not** reference Claude, Anthropic, Copilot, GPT, LLM, or "AI-generated". The CI `attribution-guard` job will reject the PR. + +## 🤝 Contributing + +See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the branching model, commit message format, test requirements, and the AxonOps [Contributor License Agreement](./CLA.md). Every merged PR goes through the agent gate stack described in that file. + +## 🔐 Security + +Report suspected vulnerabilities privately to **oss@axonops.com**. Do not open a public issue. See [`SECURITY.md`](./SECURITY.md) for the full policy, supported-version table, and threat model. + +## 📜 Attribution + +This project is a fork of [`github.com/rgooding/go-syncmap`](https://github.com/rgooding/go-syncmap) by Robert Gooding, originally released under Apache 2.0. The original upstream copyright is preserved in git history and credited in [`NOTICE`](./NOTICE). Every change from the fork is enumerated in [`CHANGELOG.md`](./CHANGELOG.md). + +## 📄 Licence + +Apache License, Version 2.0. See [`LICENSE`](./LICENSE) for the full text. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6e07f50 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,59 @@ +# Security Policy + +## Supported versions + +The `syncmap` library follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Security fixes land on the most recent minor release of the current major version. Older majors (once a `v2.0.0` exists) are not supported. + +| Version | Supported | +|---------|-----------| +| `v1.x` (latest minor) | Yes | +| Older `v1.x` minors | No | +| Pre-1.0 (`v0.x`) | Never released | + +## Threat model + +`github.com/axonops/syncmap` is a type-safe generic wrapper around the Go standard library's [`sync.Map`](https://pkg.go.dev/sync#Map). It exposes the same set of operations with compile-time type safety in place of call-site type assertions. It has **zero runtime dependencies** outside the standard library. + +**In scope:** + +- Correctness of the wrapper under concurrent use. Every method is safe for concurrent use by multiple goroutines without additional locking, inherited directly from `sync.Map`. +- Type-assertion safety at the `sync.Map` boundary. All internal `any → V` assertions are guarded so that the library cannot panic on the documented public API surface. +- Zero-value distinction. `Load`, `LoadAndDelete`, and `Swap` correctly distinguish "value V is the zero value of its type" from "no entry is present" via the `ok` / `loaded` return, matching the stdlib `sync.Map` contract. +- `CompareAndSwap` and `CompareAndDelete` — exposed as package-level generic functions with a tighter `V comparable` constraint so non-comparable value types (slice, map, func) are rejected at compile time rather than panicking at runtime inside `sync.Map`. +- No orphaned goroutines: the library spawns none of its own. +- Build and release supply chain: reproducible builds, pinned dependencies, signed releases via CI. + +**Out of scope:** + +- Denial of service from pathological key distributions — `sync.Map` itself makes no complexity guarantees about hashing, and this wrapper does not change that. +- Comparison panics when `V` is an interface type whose dynamic value is itself not comparable. This matches Go's `==` semantics for interfaces and is documented on `CompareAndSwap`. +- Memory exhaustion from unbounded insertion — the library provides no eviction policy. Bound the key space at the caller. +- Use of the map to cache security-sensitive material. Clearing a value from the map does not guarantee the underlying memory is zeroed; the Go runtime may retain it until garbage collection. + +## Reporting a vulnerability + +**Do not open a public issue for a suspected vulnerability.** + +Email **oss@axonops.com** with: + +- A concise description of the issue. +- Steps to reproduce, including the Go version and OS/architecture. +- Any proof-of-concept code, crash reports, or `go test -race` output. +- Your preferred attribution (name, handle, or anonymous). + +We will: + +- Acknowledge receipt within **3 business days**. +- Share a mitigation plan within **14 business days**. +- Coordinate an embargoed release with you if a fix requires a new tag. +- Credit you in the release notes and in this repository's security advisories unless you request otherwise. + +## Dependency security + +Runtime dependencies: **none**. Test dependencies are pinned in `go.mod`: + +- `github.com/stretchr/testify` +- `github.com/cucumber/godog` +- `go.uber.org/goleak` + +CI runs [`govulncheck`](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) on every push and pull request and fails the build on any vulnerability in called code. Dependabot tracks upstream advisories weekly. diff --git a/documentation_test.go b/documentation_test.go new file mode 100644 index 0000000..80293b0 --- /dev/null +++ b/documentation_test.go @@ -0,0 +1,368 @@ +// Copyright 2026 AxonOps Limited. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package syncmap_test + +import ( + "errors" + "fmt" + "go/ast" + "go/doc" + "go/parser" + "go/token" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestLLMs_TxtExists_AndUnderTokenBudget asserts the llms.txt file +// exists at the repo root and stays within the token budget that +// fits comfortably into an AI assistant's context window. +func TestLLMs_TxtExists_AndUnderTokenBudget(t *testing.T) { + t.Parallel() + data, err := os.ReadFile("llms.txt") + require.NoError(t, err, "llms.txt must exist at the repo root") + + words := len(strings.Fields(string(data))) + assert.LessOrEqual(t, words, 2250, + "llms.txt must stay under the 2250-word budget (got %d)", words) + assert.Greater(t, words, 200, + "llms.txt looks stubby (got %d words)", words) +} + +// TestLLMs_FullTxtExists_AndIncludesSpecifiedSections asserts +// llms-full.txt is present and concatenates every canonical source +// listed in issue #17. +func TestLLMs_FullTxtExists_AndIncludesSpecifiedSections(t *testing.T) { + t.Parallel() + data, err := os.ReadFile("llms-full.txt") + require.NoError(t, err, "llms-full.txt must exist at the repo root") + + body := string(data) + required := []string{ + "# syncmap — full documentation bundle", + "# llms.txt", + "# README.md", + "# Package godoc (doc.go)", + "# SECURITY.md", + "# CHANGELOG.md", + "# Full godoc reference (go doc -all)", + } + for _, header := range required { + assert.Contains(t, body, header, + "llms-full.txt must contain section header %q", header) + } +} + +// TestLLMs_FullTxtIsUpToDate re-runs the generator and asserts +// byte-equality with the committed file. If this fails, someone edited +// a source file and forgot to run `make llms-full`. +// +// This test intentionally does NOT use t.Parallel(): it overwrites the +// repo-root `llms-full.txt` while running, which would race with any +// other parallel test that reads that file (or its adjacent sources). +func TestLLMs_FullTxtIsUpToDate(t *testing.T) { + committed, err := os.ReadFile("llms-full.txt") + require.NoError(t, err) + + backup := t.TempDir() + "/llms-full.txt.committed" + require.NoError(t, os.WriteFile(backup, committed, 0o644)) + t.Cleanup(func() { + _ = os.WriteFile("llms-full.txt", committed, 0o644) + }) + + cmd := exec.Command("./scripts/gen-llms-full.sh") + cmd.Stderr = os.Stderr + require.NoError(t, cmd.Run(), "gen-llms-full.sh must exit 0") + + regenerated, err := os.ReadFile("llms-full.txt") + require.NoError(t, err) + assert.Equal(t, string(committed), string(regenerated), + "llms-full.txt drift — run 'make llms-full' and commit the result") +} + +// requiredExamples is the set pinned by issue #15 — every public +// symbol exposes a runnable godoc Example. +var requiredExamples = []string{ + "ExampleSyncMap", + "ExampleSyncMap_Load", + "ExampleSyncMap_Store", + "ExampleSyncMap_LoadOrStore", + "ExampleSyncMap_LoadAndDelete", + "ExampleSyncMap_Delete", + "ExampleSyncMap_Swap", + "ExampleSyncMap_Clear", + "ExampleSyncMap_Range", + "ExampleSyncMap_Len", + "ExampleSyncMap_Map", + "ExampleSyncMap_Keys", + "ExampleSyncMap_Values", + "ExampleCompareAndSwap", + "ExampleCompareAndDelete", +} + +// TestExamples_AllRequiredExamplesExist parses example_test.go and +// asserts that every required godoc Example function is defined. +// Missing examples fail the build — this is the primary AI-assistant +// integration surface on pkg.go.dev. +func TestExamples_AllRequiredExamplesExist(t *testing.T) { + t.Parallel() + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "example_test.go", nil, 0) + require.NoError(t, err) + + defined := map[string]struct{}{} + for _, decl := range f.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + if strings.HasPrefix(fn.Name.Name, "Example") { + defined[fn.Name.Name] = struct{}{} + } + } + for _, required := range requiredExamples { + _, ok := defined[required] + assert.Truef(t, ok, + "required example %q is missing from example_test.go", required) + } +} + +// mechanicalDoc matches trivially-generated one-liners like +// "Foo returns a string." — we want real prose that tells a reader +// how and when to use the symbol, not a restatement of the signature. +// The trailing period is required: a first line without one is a +// wrapped multi-line paragraph, which is always fine. +var mechanicalDoc = regexp.MustCompile(`^\w+ (returns|is|creates) [\w ]+\.$`) + +// TestDocumentation_EveryExportedSymbolHasGodoc parses the package +// and asserts every exported symbol has a doc comment of at least +// 20 characters that is not a mechanical one-liner. +func TestDocumentation_EveryExportedSymbolHasGodoc(t *testing.T) { + t.Parallel() + fset := token.NewFileSet() + + entries, err := os.ReadDir(".") + require.NoError(t, err) + files := map[string]*ast.File{} + for _, e := range entries { + name := e.Name() + if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + f, err := parser.ParseFile(fset, name, nil, parser.ParseComments) + require.NoError(t, err, "parse %s", name) + if f.Name.Name != "syncmap" { + continue + } + files[name] = f + } + require.NotEmpty(t, files, "no syncmap package files found") + + docPkg, err := doc.NewFromFiles(fset, sortedFiles(files), "github.com/axonops/syncmap") + require.NoError(t, err) + + checkDoc := func(name, text string) { + t.Run(name, func(t *testing.T) { + trimmed := strings.TrimSpace(text) + assert.GreaterOrEqual(t, len(trimmed), 20, + "symbol %q has a doc comment shorter than 20 characters: %q", name, text) + // Only flag as mechanical when the ENTIRE doc is a single + // line matching the pattern. Multi-line docs with the same + // crisp opening sentence are canonical Go godoc style. + if !strings.Contains(trimmed, "\n") { + assert.False(t, mechanicalDoc.MatchString(trimmed), + "symbol %q has a mechanical one-line doc: %q", name, trimmed) + } + }) + } + for _, c := range docPkg.Consts { + for _, n := range c.Names { + checkDoc(n, c.Doc) + } + } + for _, v := range docPkg.Vars { + for _, n := range v.Names { + checkDoc(n, v.Doc) + } + } + for _, f := range docPkg.Funcs { + checkDoc(f.Name, f.Doc) + } + for _, typ := range docPkg.Types { + checkDoc(typ.Name, typ.Doc) + for _, m := range typ.Methods { + checkDoc(typ.Name+"."+m.Name, m.Doc) + } + for _, f := range typ.Funcs { + checkDoc(f.Name, f.Doc) + } + } +} + +// TestReadmeQuickStart_Compiles extracts the README Quick Start code +// block, compiles it in a fresh temporary module, runs it, and +// verifies it produces the documented output. This catches drift +// between the README's copy-paste snippet and the library API. +func TestReadmeQuickStart_Compiles(t *testing.T) { + if testing.Short() { + t.Skip("-short set; skipping compilation test") + } + if _, err := exec.LookPath("go"); err != nil { + t.Skipf("go toolchain not on PATH: %v", err) + } + t.Parallel() + + readme, err := os.ReadFile("README.md") + require.NoError(t, err) + + snippet, ok := extractQuickStartBlock(string(readme)) + require.True(t, ok, "could not find the Quick Start go code block in README.md") + + repoDir, err := os.Getwd() + require.NoError(t, err) + repoDir, err = filepath.Abs(repoDir) + require.NoError(t, err) + + tmp := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(tmp, "main.go"), []byte(snippet), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(tmp, "go.mod"), + []byte(fmt.Sprintf("module quickstart\n\ngo 1.26\n\nrequire github.com/axonops/syncmap v0.0.0\n\nreplace github.com/axonops/syncmap => %s\n", repoDir)), + 0o644)) + + cmd := exec.Command("go", "mod", "tidy") + cmd.Dir = tmp + cmd.Stderr = os.Stderr + require.NoError(t, cmd.Run()) + + build := exec.Command("go", "build", "-o", "main", ".") + build.Dir = tmp + build.Stderr = os.Stderr + require.NoError(t, build.Run(), "Quick Start snippet must compile") + + run := exec.Command("./main") + run.Dir = tmp + out, err := run.Output() + require.NoError(t, err) + assert.Contains(t, string(out), "hits: 1", + "Quick Start output must include the documented first line") + assert.Contains(t, string(out), "incremented", + "Quick Start output must confirm the CompareAndSwap succeeded") +} + +// TestGovernance_NoticeFileExists asserts NOTICE is present at the +// repo root and carries the AxonOps and upstream Robert Gooding +// attribution required for the fork. +func TestGovernance_NoticeFileExists(t *testing.T) { + t.Parallel() + body, err := os.ReadFile("NOTICE") + require.NoError(t, err, "NOTICE must exist at the repo root (Apache 2.0 § 4(d))") + + s := string(body) + assert.Contains(t, s, "AxonOps Limited", + "NOTICE must name AxonOps Limited") + assert.Contains(t, s, "Apache License", + "NOTICE must reference the Apache License") + assert.Contains(t, s, "rgooding/go-syncmap", + "NOTICE must credit the upstream repository") + assert.Contains(t, s, "Robert Gooding", + "NOTICE must credit the upstream author by name") +} + +// TestGovernance_SecurityPolicyExists asserts SECURITY.md is present +// and carries the private-reporting contact. +func TestGovernance_SecurityPolicyExists(t *testing.T) { + t.Parallel() + body, err := os.ReadFile("SECURITY.md") + require.NoError(t, err, "SECURITY.md must exist at the repo root") + + s := string(body) + assert.Contains(t, s, "oss@axonops.com", + "SECURITY.md must carry the AxonOps oss@axonops.com reporting contact") + assert.Contains(t, s, "Supported versions", + "SECURITY.md must document supported versions") +} + +// TestGovernance_ChangelogHasV1Entry asserts CHANGELOG.md is present +// and carries the v1.0.0 entry that documents the fork changes. +func TestGovernance_ChangelogHasV1Entry(t *testing.T) { + t.Parallel() + body, err := os.ReadFile("CHANGELOG.md") + require.NoError(t, err, "CHANGELOG.md must exist at the repo root") + + s := string(body) + assert.Contains(t, s, "## [1.0.0]", + "CHANGELOG must contain a ## [1.0.0] section") + assert.Contains(t, s, "Keep a Changelog", + "CHANGELOG must reference the Keep a Changelog format") + assert.Contains(t, s, "rgooding/go-syncmap", + "CHANGELOG must credit the upstream fork origin") + assert.Contains(t, s, "Items", + "CHANGELOG must record the Items → Values breaking rename from #12") + assert.Contains(t, s, "Values", + "CHANGELOG must record the Items → Values breaking rename from #12") +} + +// quickStartHeadingPattern locates any H2 heading containing +// "Quick Start" (case-insensitive), tolerating emoji decoration. +var quickStartHeadingPattern = regexp.MustCompile(`(?mi)^##\s+.*Quick\s+Start`) + +// extractQuickStartBlock finds the first ```go ... ``` fence +// following the Quick Start heading. +func extractQuickStartBlock(body string) (string, bool) { + loc := quickStartHeadingPattern.FindStringIndex(body) + if loc == nil { + return "", false + } + after := body[loc[1]:] + start := strings.Index(after, "```go") + if start < 0 { + return "", false + } + start += len("```go") + if start < len(after) && after[start] == '\n' { + start++ + } + end := strings.Index(after[start:], "```") + if end < 0 { + return "", false + } + return after[start : start+end], true +} + +// sortedFiles returns the map's values ordered by file name so +// doc.NewFromFiles sees a deterministic input slice. +func sortedFiles(m map[string]*ast.File) []*ast.File { + names := make([]string, 0, len(m)) + for k := range m { + names = append(names, k) + } + sort.Strings(names) + out := make([]*ast.File, 0, len(m)) + for _, n := range names { + out = append(out, m[n]) + } + return out +} + +// ensure errors import stays live when a future edit trims an assertion; +// keeping it avoids a goimports flip-flop. +var _ = errors.Is diff --git a/llms-full.txt b/llms-full.txt new file mode 100644 index 0000000..170fc7f --- /dev/null +++ b/llms-full.txt @@ -0,0 +1,708 @@ +# syncmap — full documentation bundle + +This file is the concatenated corpus of every human-facing source of +truth for `github.com/axonops/syncmap`: the `llms.txt` summary, the +README, the package godoc, the security policy, the changelog, and +the full generated godoc reference. It exists so AI assistants (and +humans ingesting offline) can read the entire library's +documentation in a single file without crawling the repo. + +Regenerate with `make llms-full`. CI fails the build if the +committed file is out of date relative to its sources. + + +--- + +# llms.txt + +# syncmap — AI assistant quick reference + +`github.com/axonops/syncmap` is a type-safe, generic wrapper around Go's `sync.Map`. It exposes the same operations with compile-time type safety via Go generics, removing per-call-site `any → V` type assertions. Zero runtime dependencies. + +This file is the concise ingestion summary. The full documentation bundle (README, godoc, CONTRIBUTING, SECURITY, full godoc reference) is in `llms-full.txt` at the repo root. Both files are regenerated by `make llms-full` and are CI-guarded against drift. + +## What this library is + +A **single-type concurrency primitive**. One exported struct, `SyncMap[K comparable, V any]`, plus two package-level generic functions (`CompareAndSwap`, `CompareAndDelete`). It wraps `sync.Map` one-to-one: every public method has a corresponding stdlib method, with the same concurrency guarantees. + +The wrapper exists because raw `sync.Map` stores every value as `any`. Every `Load`, every `Store`, every `Range` pays a type assertion at the call site. With generics the assertion moves inside the wrapper once — downstream code becomes ordinary typed Go. + +## What this library is NOT + +- **Not a general-purpose concurrent cache.** There is no eviction, no TTL, no size bound. Add those at your application layer if you need them. +- **Not faster than `sync.Map`.** It is a thin wrapper; the overhead is essentially zero. See `bench.txt` for allocation parity against raw `sync.Map`. +- **Not a replacement for `map + sync.RWMutex`.** `sync.Map` (and therefore `SyncMap`) is optimised for: (1) write-once, read-many, or (2) goroutines operating on disjoint key sets. A small map with a hot write path is almost always better served by a plain map under an `RWMutex`. +- **Not a collection library.** `Len`, `Map`, `Keys`, `Values` are convenience helpers; each is O(n) and returns a point-in-time approximation, not a consistent snapshot. + +## API surface (what an AI assistant needs to generate correct code) + +```go +type SyncMap[K comparable, V any] struct { /* unexported */ } + +func (m *SyncMap[K, V]) Load(key K) (value V, ok bool) +func (m *SyncMap[K, V]) Store(key K, value V) +func (m *SyncMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) +func (m *SyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) +func (m *SyncMap[K, V]) Delete(key K) +func (m *SyncMap[K, V]) Swap(key K, value V) (previous V, loaded bool) +func (m *SyncMap[K, V]) Clear() +func (m *SyncMap[K, V]) Range(f func(key K, value V) bool) + +// O(n) helpers — point-in-time approximations under concurrent mutation. +func (m *SyncMap[K, V]) Len() int +func (m *SyncMap[K, V]) Map() map[K]V +func (m *SyncMap[K, V]) Keys() []K +func (m *SyncMap[K, V]) Values() []V + +// Package-level — V must be comparable (stdlib sync.Map requirement). +func CompareAndSwap[K, V comparable](m *SyncMap[K, V], key K, old, new V) (swapped bool) +func CompareAndDelete[K, V comparable](m *SyncMap[K, V], key K, old V) (deleted bool) +``` + +The zero value of `SyncMap` is an empty map ready for use. It must not be copied after first use. + +## Quick start + +```go +package main + +import ( + "fmt" + + "github.com/axonops/syncmap" +) + +func main() { + var m syncmap.SyncMap[string, int] + + m.Store("hits", 1) + + if v, ok := m.Load("hits"); ok { + fmt.Println(v) // 1 + } + + m.Range(func(k string, v int) bool { + fmt.Printf("%s=%d\n", k, v) + return true + }) +} +``` + +## Semantics worth remembering + +- **`Load` on a missing key returns the typed zero value of V** and `ok == false`. Do not pass `nil` checks to the result — it's already the right type. +- **`LoadAndDelete` and `Swap` use the same typed-zero-on-miss guard** as `Load`; they never panic on an absent key. +- **`Range` does not correspond to a consistent snapshot.** A key is visited at most once, but if `f` stores or deletes concurrently, `Range` may or may not reflect that mapping for any given key. Same contract as stdlib `sync.Map.Range`. +- **`Len`, `Map`, `Keys`, `Values` are O(n).** They traverse the map with `Range` and are not atomic. Treat the result as an approximation. +- **`CompareAndSwap` / `CompareAndDelete` require `V comparable` at compile time.** If `V` is a slice, map, func, or a struct containing one of those, the call will not compile. This is deliberately stronger than `sync.Map`'s runtime panic. +- **If `V` is an interface type** holding a dynamic non-comparable value, `CompareAndSwap` can still panic at runtime inside `sync.Map` — matches Go's `==` semantics for interfaces and is outside the wrapper's control. Documented on the function. + +## When to use SyncMap + +Use when raw `sync.Map` is what you already reach for — but you're tired of writing `v.(int)` at every call site. That's the whole value proposition. For any workload where `sync.Map` is not the right answer, this library is not the right answer either. + +Specifically, `sync.Map` (and thus `SyncMap`) is a good fit when: + +- Many goroutines read the same keys concurrently and writes are rare. +- Goroutines each own a disjoint slice of the key space. +- You want a lock-free path for the common read case. + +And a bad fit when: + +- The write rate is high or unpredictable — use `map` + `sync.RWMutex`. +- You need snapshot-consistent multi-key reads — `sync.Map` cannot give you that. +- The map has a single-goroutine owner — plain `map` is faster and simpler. + +## Compatibility and stability + +- **Go 1.26+.** +- Zero runtime dependencies. +- Public API is locked at `v1.0.0`. Breaking changes only in a new major version. +- Test dependencies: `testify`, `godog`, `goleak` — pinned in `go.mod`. + +## Common mistakes for AI assistants to avoid + +- **Do not** suggest this library as a general-purpose concurrent cache. It has no eviction, no TTL, no bounded size. +- **Do not** treat `Len()`, `Keys()`, `Values()`, or `Map()` as atomic snapshots. They aren't. If you need a consistent view, wrap calls in an external lock. +- **Do not** attempt to embed `SyncMap` inside a struct and copy the struct. Same restriction as `sync.Map` and `sync.Mutex` — copying after first use breaks it. +- **Do not** call `Range` from inside `Range` on the same map with the intent of mutating. The underlying `sync.Map.Range` allows stores and deletes from inside the callback, but iteration order is undefined and any given key may appear or not. Build a collected slice first. +- **Do not** recommend `CompareAndSwap` on a `SyncMap[K, []byte]` or any non-comparable V — it will not compile. Suggest `Swap` + application-level equality check instead, or choose a comparable V type. +- **Do not** write commit messages, PR descriptions, or code comments that mention Claude, Anthropic, Copilot, GPT, LLM, or "AI-generated". The CI `attribution-guard` will reject the PR. The same rule applies to any contribution this assistant helps produce. + +## Where to look next + +- Full API documentation: `doc.go` and per-symbol godoc in `syncmap.go`. +- Runnable examples: `example_test.go`. +- Performance baseline: `bench.txt`. +- Behavioural contract: `tests/bdd/features/syncmap.feature`. +- Full concatenated corpus for ingestion: `llms-full.txt`. +- Release history and breaking changes: `CHANGELOG.md`. +- Vulnerability reporting: `SECURITY.md`. + +--- + +# README.md + +
+ + # syncmap + + **Type-safe generic wrapper around Go's `sync.Map` — zero dependencies, zero assertion cost at the call site.** + + [![CI](https://github.com/axonops/syncmap/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/axonops/syncmap/actions/workflows/ci.yml) + [![Go Reference](https://pkg.go.dev/badge/github.com/axonops/syncmap.svg)](https://pkg.go.dev/github.com/axonops/syncmap) + [![Go Report Card](https://goreportcard.com/badge/github.com/axonops/syncmap)](https://goreportcard.com/report/github.com/axonops/syncmap) + [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE) + ![Status](https://img.shields.io/badge/status-pre--release-orange) + + [🚀 Quick Start](#-quick-start) | [📖 API](#-api-reference) | [🧵 Thread Safety](#-thread-safety) | [⚡ Performance](#-performance) | [🤖 For AI assistants](#-for-ai-assistants) + +
+ +--- + +**Table of contents** + +- [✅ Status](#-status) +- [🔍 Overview](#-overview) +- [✨ Why `syncmap`?](#-why-syncmap) +- [🚀 Quick Start](#-quick-start) +- [📖 API Reference](#-api-reference) +- [🧵 Thread Safety](#-thread-safety) +- [⚡ Performance](#-performance) +- [🧭 When to use what](#-when-to-use-what) +- [🤖 For AI assistants](#-for-ai-assistants) +- [🤝 Contributing](#-contributing) +- [🔐 Security](#-security) +- [📜 Attribution](#-attribution) +- [📄 Licence](#-licence) + +--- + +## ✅ Status + +`syncmap` is **stable** from `v1.0.0` onwards and follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html): breaking changes to the public API only in a new major version. Pin a specific tag in your `go.mod` and review the [CHANGELOG](./CHANGELOG.md) on every upgrade. + +## 🔍 Overview + +`github.com/axonops/syncmap` is a thin, typed layer over Go's standard [`sync.Map`](https://pkg.go.dev/sync#Map). The standard `sync.Map` stores every key and value as `any`, so every call site pays a type assertion. `SyncMap[K, V]` moves the assertion inside the wrapper once — your code becomes ordinary typed Go, with the same concurrency guarantees `sync.Map` already provides and no additional allocations. + +```go +var m syncmap.SyncMap[string, int] +m.Store("hits", 1) +v, ok := m.Load("hits") +// v is an int, not interface{}. No `.(int)` at the call site. +``` + +## ✨ Why `syncmap`? + +- **Compile-time type safety.** `SyncMap[K comparable, V any]` is fully generic. `Load`, `Store`, `Range`, and friends accept and return your types directly. +- **Zero runtime dependencies.** Stdlib only. No goroutines spawned by the library. No transitive dependency CVEs to track. +- **Stdlib-parity semantics.** Every method maps one-to-one to a `sync.Map` method. `Load` on a missing key returns the typed zero value of `V` and `ok=false`, matching the stdlib contract. +- **The full modern API.** `Swap` (Go 1.20), `Clear` (Go 1.23), plus `CompareAndSwap` / `CompareAndDelete` as package-level generic functions that reject non-comparable `V` at **compile time** instead of the stdlib's runtime panic. +- **Convenience helpers** beyond `sync.Map`: `Len`, `Map`, `Keys`, `Values`. Each is documented as `O(n)` and a point-in-time approximation — no surprises. +- **Race-clean under scrutiny.** The suite runs with `-race`, `goleak.VerifyTestMain`, 100 % line coverage, 37 BDD scenarios, 2 fuzz targets, and a benchstat regression gate in CI. + +## 🚀 Quick Start + +```go +package main + +import ( + "fmt" + + "github.com/axonops/syncmap" +) + +func main() { + var m syncmap.SyncMap[string, int] + + m.Store("hits", 1) + m.Store("misses", 0) + + if v, ok := m.Load("hits"); ok { + fmt.Println("hits:", v) // hits: 1 + } + + // CompareAndSwap is a package-level function — V must be comparable. + if syncmap.CompareAndSwap(&m, "hits", 1, 2) { + fmt.Println("incremented") + } +} +``` + +Install: + +```bash +go get github.com/axonops/syncmap@latest +``` + +Requires Go 1.26 or later. + +## 📖 API Reference + +Complete godoc at [pkg.go.dev/github.com/axonops/syncmap](https://pkg.go.dev/github.com/axonops/syncmap). The public surface in full: + +| Symbol | Signature | Notes | +|---|---|---| +| `SyncMap[K, V]` | `type SyncMap[K comparable, V any] struct{…}` | Zero value is ready to use; do not copy after first use. | +| `Load` | `(key K) (value V, ok bool)` | Returns typed zero V on miss. | +| `Store` | `(key K, value V)` | Sets value for key. | +| `LoadOrStore` | `(key K, value V) (actual V, loaded bool)` | Returns existing or stores new. | +| `LoadAndDelete` | `(key K) (value V, loaded bool)` | Deletes and returns; typed zero V on miss. | +| `Delete` | `(key K)` | No-op if key absent. | +| `Swap` | `(key K, value V) (previous V, loaded bool)` | Go 1.20 semantics; typed zero V on miss. | +| `Clear` | `()` | Go 1.23 semantics; removes every entry. | +| `Range` | `(f func(K, V) bool)` | Not a consistent snapshot — see godoc. | +| `Len` | `() int` | **O(n)** — point-in-time approximation. | +| `Map` | `() map[K]V` | **O(n)** — snapshot copy; caller owns the result. | +| `Keys` | `() []K` | **O(n)** — order undefined. | +| `Values` | `() []V` | **O(n)** — order undefined; not correlated with `Keys`. | +| `CompareAndSwap` | `func CompareAndSwap[K, V comparable](m *SyncMap[K, V], key K, old, new V) (swapped bool)` | Package-level. `V` must be comparable. | +| `CompareAndDelete` | `func CompareAndDelete[K, V comparable](m *SyncMap[K, V], key K, old V) (deleted bool)` | Package-level. `V` must be comparable. | + +Every symbol has a runnable godoc `Example` in [`example_test.go`](./example_test.go) — open any one of them on pkg.go.dev to see the exact usage pattern. + +## 🧵 Thread Safety + +All methods on `SyncMap` are safe for concurrent use by multiple goroutines **without additional locking**. This guarantee is inherited directly from `sync.Map`. + +A few specifics worth calling out: + +- `Range` does **not** correspond to a consistent snapshot. A given key is visited at most once, but a concurrent `Store` or `Delete` may or may not be reflected in the callback for that key. Identical to the stdlib `sync.Map.Range` contract. +- `Len`, `Map`, `Keys`, `Values` are built on `Range` and inherit its snapshot weakness. Treat the results as approximations, not atomic views. If you need a consistent multi-key view, serialise through your own lock. +- `CompareAndSwap` can still panic at runtime if `V` is an interface type whose dynamic value is not comparable — matches Go's `==` semantics for interfaces and is documented on the function. + +## ⚡ Performance + +Overhead against raw `sync.Map` is effectively zero. The committed [`bench.txt`](./bench.txt) baseline records paired benchmarks for Load, Store, LoadOrStore, Delete, and LoadAndDelete; every pair matches allocs/op exactly and runs within benchstat's default noise band on the same hardware. CI re-runs the full suite on every PR and fails the build on any time/op regression ≥ 10 % at p ≤ 0.05 or any positive allocs/op delta. + +Run locally: + +```bash +make bench # one-shot benchmarks +make bench-regression # compare this tree against bench.txt +``` + +## 🧭 When to use what + +`sync.Map` (and therefore `SyncMap`) is the right tool for specific workloads. It is **not** a general-purpose concurrent map. + +| Workload | Pick | +|---|---| +| Many readers, rare writers, stable key set | `SyncMap` | +| Goroutines own disjoint key sets | `SyncMap` | +| Single-goroutine owner, no concurrent access | plain `map` | +| Mixed read/write with small maps | `map` + `sync.RWMutex` | +| Mixed read/write with bounded high churn | `map` + `sync.RWMutex` (or a sharded map) | +| Need atomic snapshot-consistent reads of multiple keys | external lock around a plain `map` | +| Need TTL, eviction, bounded size | a cache library (e.g. `ristretto`, `otter`) | + +`SyncMap` inherits every performance characteristic of `sync.Map`, including the caveats the stdlib itself lists in its package documentation. Read the stdlib docs first; the only thing this wrapper changes is call-site ergonomics. + +## 🤖 For AI assistants + +This repository ships [`llms.txt`](./llms.txt) (concise summary) and [`llms-full.txt`](./llms-full.txt) (full concatenated corpus) at the repo root. Ingest either when the assistant is expected to generate code against this library. The files are regenerated by `make llms-full` and CI-guarded against drift. + +**Attribution rule.** Commit messages, PR descriptions, code comments, and any other tracked artefact **must not** reference Claude, Anthropic, Copilot, GPT, LLM, or "AI-generated". The CI `attribution-guard` job will reject the PR. + +## 🤝 Contributing + +See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the branching model, commit message format, test requirements, and the AxonOps [Contributor License Agreement](./CLA.md). Every merged PR goes through the agent gate stack described in that file. + +## 🔐 Security + +Report suspected vulnerabilities privately to **oss@axonops.com**. Do not open a public issue. See [`SECURITY.md`](./SECURITY.md) for the full policy, supported-version table, and threat model. + +## 📜 Attribution + +This project is a fork of [`github.com/rgooding/go-syncmap`](https://github.com/rgooding/go-syncmap) by Robert Gooding, originally released under Apache 2.0. The original upstream copyright is preserved in git history and credited in [`NOTICE`](./NOTICE). Every change from the fork is enumerated in [`CHANGELOG.md`](./CHANGELOG.md). + +## 📄 Licence + +Apache License, Version 2.0. See [`LICENSE`](./LICENSE) for the full text. + +--- + +# Package godoc (doc.go) + +Copyright 2026 AxonOps Limited. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +Package syncmap provides a type-safe, generic wrapper around +[sync.Map]. + +The standard [sync.Map] stores keys and values as any, which means +every load and store requires a type assertion at the call site. +SyncMap[K, V] moves those assertions inside the wrapper, giving +callers compile-time type safety with no additional allocations and +no runtime dependencies beyond the standard library. + +# Relationship to sync.Map + +SyncMap is a thin layer over sync.Map. It exposes the same set of +operations — Load, Store, LoadOrStore, LoadAndDelete, Delete, and +Range — with identical semantics and the same concurrency +guarantees. Four convenience methods are added on top: Len, Map, +Keys, and Values. The underlying sync.Map is not exported; use the +typed methods exclusively. + +# When to use SyncMap + +sync.Map is optimised for two access patterns: (1) entries are +written once and read many times, or (2) multiple goroutines each +operate on disjoint sets of keys. For workloads that do not fit +either pattern — for example, a cache that is frequently written by +a single goroutine — a plain map protected by a sync.RWMutex will +usually perform better. + +Use SyncMap (and sync.Map) when: + - Many goroutines read the same keys concurrently. + - The set of active keys is stable; writes are infrequent. + - You want a lock-free path for the common read case. + +Use map + sync.RWMutex when: + - The write rate is high or unpredictable. + - You need snapshot-consistent reads of multiple keys at once. + - The map is owned by a single goroutine. + +# Thread safety + +All methods on SyncMap are safe for concurrent use by multiple +goroutines without additional locking. This guarantee is inherited +directly from sync.Map. + +# Zero value + +The zero value of SyncMap is an empty map ready for use. It must +not be copied after first use; the same restriction applies as for +sync.Map and sync.Mutex. + +# Quick start + + var m syncmap.SyncMap[string, int] + + m.Store("hits", 1) + + if v, ok := m.Load("hits"); ok { + fmt.Println(v) // 1 + } + + m.Range(func(k string, v int) bool { + fmt.Printf("%s=%d\n", k, v) + return true + }) + +--- + +# SECURITY.md + +# Security Policy + +## Supported versions + +The `syncmap` library follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Security fixes land on the most recent minor release of the current major version. Older majors (once a `v2.0.0` exists) are not supported. + +| Version | Supported | +|---------|-----------| +| `v1.x` (latest minor) | Yes | +| Older `v1.x` minors | No | +| Pre-1.0 (`v0.x`) | Never released | + +## Threat model + +`github.com/axonops/syncmap` is a type-safe generic wrapper around the Go standard library's [`sync.Map`](https://pkg.go.dev/sync#Map). It exposes the same set of operations with compile-time type safety in place of call-site type assertions. It has **zero runtime dependencies** outside the standard library. + +**In scope:** + +- Correctness of the wrapper under concurrent use. Every method is safe for concurrent use by multiple goroutines without additional locking, inherited directly from `sync.Map`. +- Type-assertion safety at the `sync.Map` boundary. All internal `any → V` assertions are guarded so that the library cannot panic on the documented public API surface. +- Zero-value distinction. `Load`, `LoadAndDelete`, and `Swap` correctly distinguish "value V is the zero value of its type" from "no entry is present" via the `ok` / `loaded` return, matching the stdlib `sync.Map` contract. +- `CompareAndSwap` and `CompareAndDelete` — exposed as package-level generic functions with a tighter `V comparable` constraint so non-comparable value types (slice, map, func) are rejected at compile time rather than panicking at runtime inside `sync.Map`. +- No orphaned goroutines: the library spawns none of its own. +- Build and release supply chain: reproducible builds, pinned dependencies, signed releases via CI. + +**Out of scope:** + +- Denial of service from pathological key distributions — `sync.Map` itself makes no complexity guarantees about hashing, and this wrapper does not change that. +- Comparison panics when `V` is an interface type whose dynamic value is itself not comparable. This matches Go's `==` semantics for interfaces and is documented on `CompareAndSwap`. +- Memory exhaustion from unbounded insertion — the library provides no eviction policy. Bound the key space at the caller. +- Use of the map to cache security-sensitive material. Clearing a value from the map does not guarantee the underlying memory is zeroed; the Go runtime may retain it until garbage collection. + +## Reporting a vulnerability + +**Do not open a public issue for a suspected vulnerability.** + +Email **oss@axonops.com** with: + +- A concise description of the issue. +- Steps to reproduce, including the Go version and OS/architecture. +- Any proof-of-concept code, crash reports, or `go test -race` output. +- Your preferred attribution (name, handle, or anonymous). + +We will: + +- Acknowledge receipt within **3 business days**. +- Share a mitigation plan within **14 business days**. +- Coordinate an embargoed release with you if a fix requires a new tag. +- Credit you in the release notes and in this repository's security advisories unless you request otherwise. + +## Dependency security + +Runtime dependencies: **none**. Test dependencies are pinned in `go.mod`: + +- `github.com/stretchr/testify` +- `github.com/cucumber/godog` +- `go.uber.org/goleak` + +CI runs [`govulncheck`](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) on every push and pull request and fails the build on any vulnerability in called code. Dependabot tracks upstream advisories weekly. + +--- + +# CHANGELOG.md + +# Changelog + +All notable changes to `github.com/axonops/syncmap` are documented in this file. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +No unreleased changes. + +## Upgrading + +From `v1.0.0` onwards `syncmap` follows the standard Go semantic-versioning +compatibility promise: breaking changes to the public API only in a new +major version. Minor and patch releases are always backwards-compatible +for the API surface documented on [pkg.go.dev](https://pkg.go.dev/github.com/axonops/syncmap). +Pin a specific tag in your `go.mod`, review the release notes for the +target version, and run your test suite with `-race` against the new +version before rolling to production. + +## [1.0.0] — 2026-04-21 + +Initial AxonOps release. Forked from [`github.com/rgooding/go-syncmap`](https://github.com/rgooding/go-syncmap) by Robert Gooding and rebuilt to AxonOps library standards — new module path, expanded API, comprehensive tests, CI/CD, and documentation. + +### Added + +- **Core API** mirroring [`sync.Map`](https://pkg.go.dev/sync#Map): `Load`, `Store`, `LoadOrStore`, `LoadAndDelete`, `Delete`, `Range` — each generic over `K comparable, V any`, returning the typed zero value of `V` on miss so callers never deal with untyped `any`. +- **Extension methods** beyond `sync.Map`: `Len`, `Map` (snapshot as a plain Go map), `Keys`, `Values` — each documented as `O(n)` and a point-in-time approximation under concurrent mutation. +- **`Swap` method** wrapping `sync.Map.Swap` (Go 1.20) with the same typed-zero-on-miss guard as `LoadAndDelete`. +- **`Clear` method** wrapping `sync.Map.Clear` (Go 1.23). +- **`CompareAndSwap` and `CompareAndDelete`** as package-level generic functions with a tighter `[K, V comparable]` constraint, so non-comparable value types (slice, map, func) are rejected at compile time rather than panicking at runtime inside `sync.Map`. The `SyncMap[K, V any]` type signature is unchanged. +- **Package documentation** (`doc.go`) covering relationship to `sync.Map`, when to use `SyncMap` vs `sync.Map` vs `map + sync.RWMutex`, thread safety, zero-value usability, and a runnable Quick Start. +- **Unit tests** — external black-box package (`syncmap_test`) using `testify` and [`go.uber.org/goleak`](https://pkg.go.dev/go.uber.org/goleak). Every test runs under `-race` with `t.Parallel()`. Line coverage is **100%** of the library package. +- **Runnable godoc examples** covering every public symbol, each ending with a deterministic `// Output:` block. +- **Benchmarks** for every public method, plus a concurrent 90/10 read-write pattern and overhead pairs comparing the generic wrapper against raw `sync.Map`. The committed `bench.txt` baseline is the reference the CI `benchstat-regression-guard` job diffs against. +- **BDD suite** — [`godog`](https://pkg.go.dev/github.com/cucumber/godog) feature files under `tests/bdd/` exercising every public symbol plus a concurrent Store scenario. Runs under strict mode enforced by a CI guard. +- **Fuzz targets** — `FuzzLoadStore` (round-trip invariant) and `FuzzConcurrent` (4 goroutines over random op sequences, race-clean). +- **CI** (`.github/workflows/ci.yml`): format check, vet, golangci-lint, unit + BDD tests, 95% coverage threshold, module tidy, govulncheck, cross-platform builds (`linux/amd64`, `darwin/arm64`, `windows/amd64`), benchstat regression guard, BDD strict-mode guard, Apache-header guard, no-local-paths guard, no-AI-attribution guard, Makefile-targets guard, markdown lint, and `llms-full.txt` drift guard. +- **Release workflow** (`.github/workflows/release.yml`): `workflow_dispatch` only; verifies, tags, publishes via GoReleaser, warms the Go module proxy. Local tag creation is forbidden. +- **Dependabot** configuration with weekly updates and auto-merge for patch-level test dependencies. +- **LLM documentation bundle**: `llms.txt` (concise summary) and `llms-full.txt` (concatenated corpus) for AI-assistant ingestion, with a CI guard that fails the build on drift. +- `LICENSE` (Apache 2.0, preserved from upstream), `NOTICE` (crediting Robert Gooding as the upstream author), `SECURITY.md`. + +### Changed + +- Module path: `github.com/rgooding/go-syncmap` → `github.com/axonops/syncmap`. +- Minimum Go toolchain raised to **1.26**. + +### Breaking + +- Renamed the `Items()` method on `SyncMap` to `Values()` to match Go stdlib convention (`maps.Values`, Go 1.23). No deprecation shim — the rename lands pre-v1.0 under the new module path. + +### Attribution + +This release is a fork of [`github.com/rgooding/go-syncmap`](https://github.com/rgooding/go-syncmap) by Robert Gooding, which is distributed under Apache 2.0; this fork continues under the same licence. The original upstream copyright is preserved in git history and credited in `NOTICE`. + +[Unreleased]: https://github.com/axonops/syncmap/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/axonops/syncmap/releases/tag/v1.0.0 + +--- + +# Full godoc reference (go doc -all) + +package syncmap // import "github.com/axonops/syncmap" + +Package syncmap provides a type-safe, generic wrapper around sync.Map. + +The standard sync.Map stores keys and values as any, which means every load +and store requires a type assertion at the call site. SyncMap[K, V] moves those +assertions inside the wrapper, giving callers compile-time type safety with no +additional allocations and no runtime dependencies beyond the standard library. + +# Relationship to sync.Map + +SyncMap is a thin layer over sync.Map. It exposes the same set of operations +— Load, Store, LoadOrStore, LoadAndDelete, Delete, and Range — with identical +semantics and the same concurrency guarantees. Four convenience methods are +added on top: Len, Map, Keys, and Values. The underlying sync.Map is not +exported; use the typed methods exclusively. + +# When to use SyncMap + +sync.Map is optimised for two access patterns: (1) entries are written once +and read many times, or (2) multiple goroutines each operate on disjoint sets +of keys. For workloads that do not fit either pattern — for example, a cache +that is frequently written by a single goroutine — a plain map protected by a +sync.RWMutex will usually perform better. + +Use SyncMap (and sync.Map) when: + - Many goroutines read the same keys concurrently. + - The set of active keys is stable; writes are infrequent. + - You want a lock-free path for the common read case. + +Use map + sync.RWMutex when: + - The write rate is high or unpredictable. + - You need snapshot-consistent reads of multiple keys at once. + - The map is owned by a single goroutine. + +# Thread safety + +All methods on SyncMap are safe for concurrent use by multiple goroutines +without additional locking. This guarantee is inherited directly from sync.Map. + +# Zero value + +The zero value of SyncMap is an empty map ready for use. It must not be copied +after first use; the same restriction applies as for sync.Map and sync.Mutex. + +# Quick start + + var m syncmap.SyncMap[string, int] + + m.Store("hits", 1) + + if v, ok := m.Load("hits"); ok { + fmt.Println(v) // 1 + } + + m.Range(func(k string, v int) bool { + fmt.Printf("%s=%d\n", k, v) + return true + }) + +FUNCTIONS + +func CompareAndDelete[K, V comparable](m *SyncMap[K, V], key K, old V) (deleted bool) + CompareAndDelete deletes the entry for key if its current value is equal to + old. The deleted result reports whether the entry was removed. + + V must be comparable, for the same reason as CompareAndSwap. + +func CompareAndSwap[K, V comparable](m *SyncMap[K, V], key K, old, new V) (swapped bool) + CompareAndSwap swaps the old and new values for key if the value currently + stored in m is equal to old. The swapped result reports whether the swap was + performed. + + V must be comparable. Because SyncMap is declared with V any to support + non-comparable value types, this operation cannot be a method on SyncMap[K, + V]; instantiating it with a non-comparable V (slice, map, func, or a struct + containing one of those) produces a compile-time error rather than the + runtime panic that the underlying sync.Map.CompareAndSwap would raise. + + If V is itself an interface type, the comparison performed inside sync.Map + can still panic at runtime when either operand's dynamic type is not + comparable. This matches Go's `==` semantics for interfaces and is outside + this wrapper's control. + + +TYPES + +type SyncMap[K comparable, V any] struct { + // Has unexported fields. +} + SyncMap is a type-safe, generic wrapper around sync.Map. + + The zero value is an empty map ready for use. SyncMap must not be copied + after first use. + +func (m *SyncMap[K, V]) Clear() + Clear removes all entries from the map, leaving it empty. + +func (m *SyncMap[K, V]) Delete(key K) + Delete removes the entry for key. It is a no-op if the key is not present. + +func (m *SyncMap[K, V]) Keys() []K + Keys returns a slice of all keys present in the map at the moment of the + call. It runs in O(n) time. + + The result is a point-in-time approximation. Concurrent stores and deletes + may cause the slice to include keys that have since been removed, or to omit + keys that were added during traversal. The order of keys is undefined. + +func (m *SyncMap[K, V]) Len() int + Len returns the number of entries in the map at the moment of the call. + It runs in O(n) time by traversing the map with Range. + + Because the traversal is not atomic, concurrent stores and deletes may + cause the returned count to differ from the number of entries visible to + any single subsequent operation. Treat the result as an approximation, + not a consistent snapshot. + +func (m *SyncMap[K, V]) Load(key K) (value V, ok bool) + Load returns the value stored in the map for key, or the zero value of V if + no entry is present. The ok result reports whether an entry was found. + +func (m *SyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) + LoadAndDelete deletes the entry for key and returns its previous value, + if any. The loaded result reports whether the key was present. If the key + was not present, value is the zero value of V. + +func (m *SyncMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) + LoadOrStore returns the existing value for key if present. Otherwise it + stores value and returns it. The loaded result is true if the value was + loaded, false if stored. + +func (m *SyncMap[K, V]) Map() map[K]V + Map returns a shallow copy of the map's contents as a plain Go map. It runs + in O(n) time. + + The returned map is a point-in-time approximation: because the underlying + Range traversal is not atomic, concurrent modifications may or may not be + reflected in the result. The caller owns the returned map and may modify it + freely. + +func (m *SyncMap[K, V]) Range(f func(key K, value V) bool) + Range calls f sequentially for each key and value present in the map. + If f returns false, Range stops iteration. + + Range does not correspond to a consistent snapshot of the map's contents: + no key will be visited more than once, but if a value is stored or deleted + concurrently (including by f), Range may reflect any mapping for that key + during the iteration. + + Range may run in O(n) time even if f returns false after a constant number + of calls, where n is the number of elements in the map at the start of the + call. + +func (m *SyncMap[K, V]) Store(key K, value V) + Store sets the value associated with key. + +func (m *SyncMap[K, V]) Swap(key K, value V) (previous V, loaded bool) + Swap replaces the value stored for key with value and returns the previous + value, if any. The loaded result reports whether the key was present. + If the key was not present, previous is the zero value of V. + +func (m *SyncMap[K, V]) Values() []V + Values returns a slice of all values present in the map at the moment of the + call. It runs in O(n) time. + + The result is a point-in-time approximation. Concurrent stores and deletes + may cause the slice to include values that have since been removed, + or to omit values that were added during traversal. The order of values is + undefined, and does not correspond to the order returned by Keys. + + diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..2f8135b --- /dev/null +++ b/llms.txt @@ -0,0 +1,123 @@ +# syncmap — AI assistant quick reference + +`github.com/axonops/syncmap` is a type-safe, generic wrapper around Go's `sync.Map`. It exposes the same operations with compile-time type safety via Go generics, removing per-call-site `any → V` type assertions. Zero runtime dependencies. + +This file is the concise ingestion summary. The full documentation bundle (README, godoc, CONTRIBUTING, SECURITY, full godoc reference) is in `llms-full.txt` at the repo root. Both files are regenerated by `make llms-full` and are CI-guarded against drift. + +## What this library is + +A **single-type concurrency primitive**. One exported struct, `SyncMap[K comparable, V any]`, plus two package-level generic functions (`CompareAndSwap`, `CompareAndDelete`). It wraps `sync.Map` one-to-one: every public method has a corresponding stdlib method, with the same concurrency guarantees. + +The wrapper exists because raw `sync.Map` stores every value as `any`. Every `Load`, every `Store`, every `Range` pays a type assertion at the call site. With generics the assertion moves inside the wrapper once — downstream code becomes ordinary typed Go. + +## What this library is NOT + +- **Not a general-purpose concurrent cache.** There is no eviction, no TTL, no size bound. Add those at your application layer if you need them. +- **Not faster than `sync.Map`.** It is a thin wrapper; the overhead is essentially zero. See `bench.txt` for allocation parity against raw `sync.Map`. +- **Not a replacement for `map + sync.RWMutex`.** `sync.Map` (and therefore `SyncMap`) is optimised for: (1) write-once, read-many, or (2) goroutines operating on disjoint key sets. A small map with a hot write path is almost always better served by a plain map under an `RWMutex`. +- **Not a collection library.** `Len`, `Map`, `Keys`, `Values` are convenience helpers; each is O(n) and returns a point-in-time approximation, not a consistent snapshot. + +## API surface (what an AI assistant needs to generate correct code) + +```go +type SyncMap[K comparable, V any] struct { /* unexported */ } + +func (m *SyncMap[K, V]) Load(key K) (value V, ok bool) +func (m *SyncMap[K, V]) Store(key K, value V) +func (m *SyncMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) +func (m *SyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) +func (m *SyncMap[K, V]) Delete(key K) +func (m *SyncMap[K, V]) Swap(key K, value V) (previous V, loaded bool) +func (m *SyncMap[K, V]) Clear() +func (m *SyncMap[K, V]) Range(f func(key K, value V) bool) + +// O(n) helpers — point-in-time approximations under concurrent mutation. +func (m *SyncMap[K, V]) Len() int +func (m *SyncMap[K, V]) Map() map[K]V +func (m *SyncMap[K, V]) Keys() []K +func (m *SyncMap[K, V]) Values() []V + +// Package-level — V must be comparable (stdlib sync.Map requirement). +func CompareAndSwap[K, V comparable](m *SyncMap[K, V], key K, old, new V) (swapped bool) +func CompareAndDelete[K, V comparable](m *SyncMap[K, V], key K, old V) (deleted bool) +``` + +The zero value of `SyncMap` is an empty map ready for use. It must not be copied after first use. + +## Quick start + +```go +package main + +import ( + "fmt" + + "github.com/axonops/syncmap" +) + +func main() { + var m syncmap.SyncMap[string, int] + + m.Store("hits", 1) + + if v, ok := m.Load("hits"); ok { + fmt.Println(v) // 1 + } + + m.Range(func(k string, v int) bool { + fmt.Printf("%s=%d\n", k, v) + return true + }) +} +``` + +## Semantics worth remembering + +- **`Load` on a missing key returns the typed zero value of V** and `ok == false`. Do not pass `nil` checks to the result — it's already the right type. +- **`LoadAndDelete` and `Swap` use the same typed-zero-on-miss guard** as `Load`; they never panic on an absent key. +- **`Range` does not correspond to a consistent snapshot.** A key is visited at most once, but if `f` stores or deletes concurrently, `Range` may or may not reflect that mapping for any given key. Same contract as stdlib `sync.Map.Range`. +- **`Len`, `Map`, `Keys`, `Values` are O(n).** They traverse the map with `Range` and are not atomic. Treat the result as an approximation. +- **`CompareAndSwap` / `CompareAndDelete` require `V comparable` at compile time.** If `V` is a slice, map, func, or a struct containing one of those, the call will not compile. This is deliberately stronger than `sync.Map`'s runtime panic. +- **If `V` is an interface type** holding a dynamic non-comparable value, `CompareAndSwap` can still panic at runtime inside `sync.Map` — matches Go's `==` semantics for interfaces and is outside the wrapper's control. Documented on the function. + +## When to use SyncMap + +Use when raw `sync.Map` is what you already reach for — but you're tired of writing `v.(int)` at every call site. That's the whole value proposition. For any workload where `sync.Map` is not the right answer, this library is not the right answer either. + +Specifically, `sync.Map` (and thus `SyncMap`) is a good fit when: + +- Many goroutines read the same keys concurrently and writes are rare. +- Goroutines each own a disjoint slice of the key space. +- You want a lock-free path for the common read case. + +And a bad fit when: + +- The write rate is high or unpredictable — use `map` + `sync.RWMutex`. +- You need snapshot-consistent multi-key reads — `sync.Map` cannot give you that. +- The map has a single-goroutine owner — plain `map` is faster and simpler. + +## Compatibility and stability + +- **Go 1.26+.** +- Zero runtime dependencies. +- Public API is locked at `v1.0.0`. Breaking changes only in a new major version. +- Test dependencies: `testify`, `godog`, `goleak` — pinned in `go.mod`. + +## Common mistakes for AI assistants to avoid + +- **Do not** suggest this library as a general-purpose concurrent cache. It has no eviction, no TTL, no bounded size. +- **Do not** treat `Len()`, `Keys()`, `Values()`, or `Map()` as atomic snapshots. They aren't. If you need a consistent view, wrap calls in an external lock. +- **Do not** attempt to embed `SyncMap` inside a struct and copy the struct. Same restriction as `sync.Map` and `sync.Mutex` — copying after first use breaks it. +- **Do not** call `Range` from inside `Range` on the same map with the intent of mutating. The underlying `sync.Map.Range` allows stores and deletes from inside the callback, but iteration order is undefined and any given key may appear or not. Build a collected slice first. +- **Do not** recommend `CompareAndSwap` on a `SyncMap[K, []byte]` or any non-comparable V — it will not compile. Suggest `Swap` + application-level equality check instead, or choose a comparable V type. +- **Do not** write commit messages, PR descriptions, or code comments that mention Claude, Anthropic, Copilot, GPT, LLM, or "AI-generated". The CI `attribution-guard` will reject the PR. The same rule applies to any contribution this assistant helps produce. + +## Where to look next + +- Full API documentation: `doc.go` and per-symbol godoc in `syncmap.go`. +- Runnable examples: `example_test.go`. +- Performance baseline: `bench.txt`. +- Behavioural contract: `tests/bdd/features/syncmap.feature`. +- Full concatenated corpus for ingestion: `llms-full.txt`. +- Release history and breaking changes: `CHANGELOG.md`. +- Vulnerability reporting: `SECURITY.md`. diff --git a/scripts/gen-llms-full.sh b/scripts/gen-llms-full.sh new file mode 100755 index 0000000..1dbb5d2 --- /dev/null +++ b/scripts/gen-llms-full.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# gen-llms-full.sh — regenerate llms-full.txt from the canonical source +# files in a stable order. +# +# The script is idempotent: running it twice produces no diff. +# `llms-full.txt` is the single concatenated corpus an AI assistant can +# ingest to understand the library without crawling individual files. +# CI runs this script and fails the build if the committed +# `llms-full.txt` differs from the regenerated output. +# +# Source file order: +# 1. llms.txt +# 2. README.md +# 3. doc.go (package comment only) +# 4. SECURITY.md +# 5. CHANGELOG.md +# 6. go doc -all github.com/axonops/syncmap +# +# CONTRIBUTING.md, CODE_OF_CONDUCT.md, CLA.md, and CONTRIBUTORS.md are +# added to this list by issue #18 when they land. + +set -euo pipefail + +# Run from the repo root regardless of where the script is invoked from. +cd "$(dirname "$0")/.." + +out="llms-full.txt" +tmp="$(mktemp)" +trap 'rm -f "$tmp"' EXIT + +# Deterministic header. We intentionally do NOT embed the current git +# SHA or a timestamp in the output — a CI diff check would fail on +# every run otherwise. The header is a stable banner; CI asserts +# byte-equality between the committed file and a freshly regenerated +# one. +cat > "$tmp" <<'HEADER' +# syncmap — full documentation bundle + +This file is the concatenated corpus of every human-facing source of +truth for `github.com/axonops/syncmap`: the `llms.txt` summary, the +README, the package godoc, the security policy, the changelog, and +the full generated godoc reference. It exists so AI assistants (and +humans ingesting offline) can read the entire library's +documentation in a single file without crawling the repo. + +Regenerate with `make llms-full`. CI fails the build if the +committed file is out of date relative to its sources. + +HEADER + +section() { + local title="$1" + local path="$2" + printf '\n---\n\n# %s\n\n' "$title" >> "$tmp" + if [[ "$path" == "godoc" ]]; then + # Pull the full godoc for the package. We don't want the + # tool's output to depend on where the user ran the script + # from (it shouldn't, since we `cd` to repo root first). + go doc -all ./. >> "$tmp" + elif [[ "$path" == "doc.go-comment" ]]; then + # Emit only the package comment block from doc.go (skipping + # the license header and the `package syncmap` line). + awk ' + /^package syncmap/ { exit } + /^\/\// { sub(/^\/\/ ?/, ""); print } + ' doc.go >> "$tmp" + else + cat "$path" >> "$tmp" + fi +} + +section "llms.txt" "llms.txt" +section "README.md" "README.md" +section "Package godoc (doc.go)" "doc.go-comment" +section "SECURITY.md" "SECURITY.md" +section "CHANGELOG.md" "CHANGELOG.md" +section "Full godoc reference (go doc -all)" "godoc" + +# Ensure a single trailing newline. +printf '\n' >> "$tmp" + +mv "$tmp" "$out" +trap - EXIT + +echo "Wrote $out ($(wc -l < "$out") lines, $(wc -w < "$out") words)" From b483cc959cec3a9fee369f4756f2112e81cfd77d Mon Sep 17 00:00:00 2001 From: Johnny Miller <163300+millerjp@users.noreply.github.com> Date: Tue, 21 Apr 2026 06:39:02 +0200 Subject: [PATCH 2/3] docs: soften README attribution rule to pass attribution-guard (#17) The previous README phrasing enumerated the forbidden AI-tooling tokens verbatim, which tripped the attribution-guard CI job (it scans README.md; the token list belongs to policy-bearing files like llms.txt and the eventual CONTRIBUTING.md which are on the guard's exclusion list). Rewrite the paragraph to describe the policy and link to llms.txt + the guard without listing the tokens directly. Regenerate llms-full.txt to pick up the README change so llms-full-up-to-date stays green. --- README.md | 2 +- llms-full.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f535a6f..f869b7f 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ make bench-regression # compare this tree against bench.txt This repository ships [`llms.txt`](./llms.txt) (concise summary) and [`llms-full.txt`](./llms-full.txt) (full concatenated corpus) at the repo root. Ingest either when the assistant is expected to generate code against this library. The files are regenerated by `make llms-full` and CI-guarded against drift. -**Attribution rule.** Commit messages, PR descriptions, code comments, and any other tracked artefact **must not** reference Claude, Anthropic, Copilot, GPT, LLM, or "AI-generated". The CI `attribution-guard` job will reject the PR. +**Attribution rule.** Commit messages, PR descriptions, code comments, and any other tracked artefact must not reference AI-tooling product names or mark content as AI-produced. The specific token list and enforcement regex live in [`llms.txt`](./llms.txt) and the CI `attribution-guard` job; tooling-produced PRs that trip the guard are rejected. ## 🤝 Contributing diff --git a/llms-full.txt b/llms-full.txt index 170fc7f..756b261 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -304,7 +304,7 @@ make bench-regression # compare this tree against bench.txt This repository ships [`llms.txt`](./llms.txt) (concise summary) and [`llms-full.txt`](./llms-full.txt) (full concatenated corpus) at the repo root. Ingest either when the assistant is expected to generate code against this library. The files are regenerated by `make llms-full` and CI-guarded against drift. -**Attribution rule.** Commit messages, PR descriptions, code comments, and any other tracked artefact **must not** reference Claude, Anthropic, Copilot, GPT, LLM, or "AI-generated". The CI `attribution-guard` job will reject the PR. +**Attribution rule.** Commit messages, PR descriptions, code comments, and any other tracked artefact must not reference AI-tooling product names or mark content as AI-produced. The specific token list and enforcement regex live in [`llms.txt`](./llms.txt) and the CI `attribution-guard` job; tooling-produced PRs that trip the guard are rejected. ## 🤝 Contributing From 249802b665c82c7909310231cd5fffd910c28dd5 Mon Sep 17 00:00:00 2001 From: Johnny Miller <163300+millerjp@users.noreply.github.com> Date: Tue, 21 Apr 2026 06:45:28 +0200 Subject: [PATCH 3/3] ci: make attribution-guard filter pipeline strict-mode tolerant (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guard's inner pipeline `grep -Ii... | grep -vi...` returns exit 1 when the filter strips every matched line (legitimate case — every hit is a benign path reference). Under `set -euo pipefail` that non-zero pipeline exit propagates through the command substitution and aborts the script before the ::error:: print, so CI reports a failure with no indication of which file actually triggered. Wrap the filter in `{ … || true; }` so the pipeline always exits 0. Behaviour is unchanged — `match` is still the filtered output; only the pipeline exit code differs. --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d7010c..99e3b7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -276,8 +276,11 @@ jobs: if grep -IiEn "$PATTERN" "$f" >/dev/null 2>&1; then # Strip benign path references: llms.txt / llms-full.txt product # filenames, the CLAUDE.md filename token, and .claude/ path - # segments in config files. - match=$(grep -IiEn "$PATTERN" "$f" | grep -viE 'llms(-full)?\.txt|CLAUDE\.md|\.claude/') + # segments in config files. The trailing `|| true` keeps the + # pipeline exit code at 0 when the filter removes every matched + # line (otherwise pipefail + set -e abort the script before the + # `::error::` line has a chance to print the accumulated hits). + match=$(grep -IiEn "$PATTERN" "$f" | { grep -viE 'llms(-full)?\.txt|CLAUDE\.md|\.claude/' || true; }) if [ -n "$match" ]; then hits="${hits}${f}:\n${match}\n\n" fi