From 5d1ee3f0eab8e1634a09266d7d3f1226a9fe4ea3 Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 4 Nov 2025 00:38:18 +0000 Subject: [PATCH] Refactor CI configuration and documentation; improve error handling in KDTree functions --- .github/workflows/ci.yml | 1 + .golangci.yml | 3 +-- CODE_OF_CONDUCT.md | 6 +++--- CONTRIBUTING.md | 2 +- Makefile | 4 ++++ docs/api.md | 2 ++ docs/dht-best-ping.md | 2 ++ docs/getting-started.md | 2 +- docs/kdtree-multidimensional.md | 19 ++++++++++++++----- docs/perf.md | 2 +- docs/wasm.md | 2 +- examples/dht_ping_1d/example_test.go | 5 +++-- examples/kdtree_2d_ping_hop/main.go | 15 ++++++++++++--- examples/kdtree_4d_ping_hop_geo_score/main.go | 15 ++++++++++++--- kdtree_helpers.go | 6 +++--- kdtree_morecov_test.go | 4 ++-- npm/poindexter-wasm/loader.js | 3 ++- 17 files changed, 65 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89f1752..46d589a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: permissions: contents: read + actions: write jobs: build-test-wasm: diff --git a/.golangci.yml b/.golangci.yml index 2fc3e88..ea9604e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -17,5 +17,4 @@ issues: - errcheck linters-settings: errcheck: - # Be pragmatic: don't require checking of Close for defer patterns in tests/examples. - # (Keep defaults; exclusions above handle test files.) + # Using default settings; test file exclusions are handled by exclude-rules above. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 58dcd62..2612a03 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,8 +2,8 @@ This project has adopted the Contributor Covenant Code of Conduct. -- Version: https://www.contributor-covenant.org/version/2/1/code_of_conduct/ -- FAQ: https://www.contributor-covenant.org/faq -- Translations: https://www.contributor-covenant.org/translations +- Version: [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) +- FAQ: [Contributor Covenant FAQ](https://www.contributor-covenant.org/faq) +- Translations: [Available Translations](https://www.contributor-covenant.org/translations) Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the maintainers via GitHub issues or by email listed on the repository profile. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad1d242..bf819b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Thanks for your interest in contributing! This document describes how to build, ## Getting started -- Go 1.22+ (1.23 preferred) +- Go 1.23+ - `git clone https://github.com/Snider/Poindexter` - `cd Poindexter` diff --git a/Makefile b/Makefile index 590912a..4c4a066 100644 --- a/Makefile +++ b/Makefile @@ -147,3 +147,7 @@ docs-serve: ## Serve MkDocs locally (requires mkdocs-material) .PHONY: docs-build docs-build: ## Build MkDocs site into site/ $(MKDOCS) build + +.PHONY: clean +clean: ## Remove generated files and directories + rm -rf $(DIST_DIR) $(COVEROUT) $(COVERHTML) $(BENCHOUT) diff --git a/docs/api.md b/docs/api.md index 8c5c9f2..da86214 100644 --- a/docs/api.md +++ b/docs/api.md @@ -470,7 +470,9 @@ func Build4DWithStats[T any]( ) ([]KDPoint[T], error) ``` + #### Example (2D) + ```go // Compute stats once over your baseline set stats := poindexter.ComputeNormStats2D(peers, diff --git a/docs/dht-best-ping.md b/docs/dht-best-ping.md index 98cec1c..ce8e2a4 100644 --- a/docs/dht-best-ping.md +++ b/docs/dht-best-ping.md @@ -101,6 +101,7 @@ func main() { ### Why does querying with `[0]` work? We use Euclidean distance in 1D, so `distance = |ping - target|`. With target `0`, minimizing the distance is equivalent to minimizing the ping itself. + ### Extending the metric/space - Multi-objective: encode more routing features (lower is better) as extra dimensions, e.g. `[ping_ms, hops, queue_delay_ms]`. - Metric choice: @@ -109,6 +110,7 @@ We use Euclidean distance in 1D, so `distance = |ping - target|`. With target `0 - `ChebyshevDistance` (L∞): cares about the worst dimension. - Normalization: when mixing units (ms, hops, km), normalize or weight dimensions so the metric reflects your priority. + ### Notes - This KDTree currently uses an internal linear scan for queries. The API is stable and designed so it can be swapped to use `gonum.org/v1/gonum/spatial/kdtree` under the hood later for sub-linear queries on large datasets. - IDs are optional but recommended for O(1)-style deletes; keep them unique per tree. diff --git a/docs/getting-started.md b/docs/getting-started.md index 0303814..0e520a9 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -123,5 +123,5 @@ func main() { - Check out the [API Reference](api.md) for detailed documentation - Try the example: [Find the best (lowest‑ping) DHT peer](dht-best-ping.md) -- Explore multi-dimensional KDTree over ping/hops/geo/score: [Multi-Dimensional KDTree (DHT)](kdtree-multidimensional.md) +- Explore multidimensional KDTree over ping/hops/geo/score: [Multidimensional KDTree (DHT)](kdtree-multidimensional.md) - Read about the [License](license.md) diff --git a/docs/kdtree-multidimensional.md b/docs/kdtree-multidimensional.md index e4cc6a5..e2cf8da 100644 --- a/docs/kdtree-multidimensional.md +++ b/docs/kdtree-multidimensional.md @@ -237,23 +237,32 @@ func main() { // Initial 2‑D build (ping + hops) weights2 := [2]float64{1.0, 1.0} invert2 := [2]bool{false, false} - pts, _ := poindexter.Build2D( + + // Compute normalization stats once over your baseline set + stats := poindexter.ComputeNormStats2D( + peers, + func(p Peer) float64 { return p.PingMS }, + func(p Peer) float64 { return p.Hops }, + ) + + // Build using the precomputed stats so future inserts share the same scale + pts, _ := poindexter.Build2DWithStats( peers, func(p Peer) string { return p.ID }, func(p Peer) float64 { return p.PingMS }, func(p Peer) float64 { return p.Hops }, - weights2, invert2, + weights2, invert2, stats, ) tree, _ := poindexter.NewKDTree(pts) - // Insert a new peer: rebuild its point using the same helper. + // Insert a new peer: reuse the same normalization stats to keep scale consistent newPeer := Peer{ID: "Z", PingMS: 12, Hops: 2} - addPts, _ := poindexter.Build2D( + addPts, _ := poindexter.Build2DWithStats( []Peer{newPeer}, func(p Peer) string { return p.ID }, func(p Peer) float64 { return p.PingMS }, func(p Peer) float64 { return p.Hops }, - weights2, invert2, + weights2, invert2, stats, ) _ = tree.Insert(addPts[0]) diff --git a/docs/perf.md b/docs/perf.md index ce21467..d3fe806 100644 --- a/docs/perf.md +++ b/docs/perf.md @@ -16,7 +16,7 @@ Run them locally: go test -bench . -benchmem -run=^$ ./... ``` -GitHub Actions publishes benchmark artifacts for Go 1.22 and 1.23 on every push/PR. Look for artifacts named `bench-.txt` in the CI run. +GitHub Actions publishes benchmark artifacts for Go 1.23 on every push/PR. Look for artifacts named `bench-.txt` in the CI run. ## What to expect (rule of thumb) diff --git a/docs/wasm.md b/docs/wasm.md index 373b793..551dc86 100644 --- a/docs/wasm.md +++ b/docs/wasm.md @@ -27,7 +27,7 @@ To assemble the npm package folder with the built artifacts: make npm-pack ``` -This populates `npm/poindexter-wasm/` with `dist/`, license and readme files. You can then create a tarball for local testing: +This populates `npm/poindexter-wasm/` with `dist/`, licence and readme files. You can then create a tarball for local testing: ```bash npm pack ./npm/poindexter-wasm diff --git a/examples/dht_ping_1d/example_test.go b/examples/dht_ping_1d/example_test.go index 3f431a0..d4ce2d8 100644 --- a/examples/dht_ping_1d/example_test.go +++ b/examples/dht_ping_1d/example_test.go @@ -42,7 +42,8 @@ func TestExample1D(t *testing.T) { if best.Value.Ping != 35 { t.Fatalf("expected best ping 35ms, got %d", best.Value.Ping) } - if d <= 0 { - t.Fatalf("expected positive distance, got %v", d) + // Distance from [0] to [35] should be 35 + if d != 35 { + t.Fatalf("expected distance 35, got %v", d) } } diff --git a/examples/kdtree_2d_ping_hop/main.go b/examples/kdtree_2d_ping_hop/main.go index 200318e..93f838d 100644 --- a/examples/kdtree_2d_ping_hop/main.go +++ b/examples/kdtree_2d_ping_hop/main.go @@ -21,14 +21,23 @@ func main() { } weights := [2]float64{1.0, 1.0} invert := [2]bool{false, false} - pts, _ := poindexter.Build2D( + pts, err := poindexter.Build2D( peers, func(p Peer2) string { return p.ID }, func(p Peer2) float64 { return p.PingMS }, func(p Peer2) float64 { return p.Hops }, weights, invert, ) - tr, _ := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.ManhattanDistance{})) - best, _, _ := tr.Nearest([]float64{0, 0.3}) + if err != nil { + panic(fmt.Sprintf("Build2D failed: %v", err)) + } + tr, err := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.ManhattanDistance{})) + if err != nil { + panic(fmt.Sprintf("NewKDTree failed: %v", err)) + } + best, _, ok := tr.Nearest([]float64{0, 0.3}) + if !ok { + panic("no nearest neighbour found") + } fmt.Println("2D best:", best.ID) } diff --git a/examples/kdtree_4d_ping_hop_geo_score/main.go b/examples/kdtree_4d_ping_hop_geo_score/main.go index 89180ca..c67b9ed 100644 --- a/examples/kdtree_4d_ping_hop_geo_score/main.go +++ b/examples/kdtree_4d_ping_hop_geo_score/main.go @@ -23,7 +23,7 @@ func main() { } weights := [4]float64{1.0, 0.7, 0.2, 1.2} invert := [4]bool{false, false, false, true} - pts, _ := poindexter.Build4D( + pts, err := poindexter.Build4D( peers, func(p Peer4) string { return p.ID }, func(p Peer4) float64 { return p.PingMS }, @@ -32,7 +32,16 @@ func main() { func(p Peer4) float64 { return p.Score }, weights, invert, ) - tr, _ := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.EuclideanDistance{})) - best, _, _ := tr.Nearest([]float64{0, weights[1] * 0.2, weights[2] * 0.3, 0}) + if err != nil { + panic(err) + } + tr, err := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.EuclideanDistance{})) + if err != nil { + panic(err) + } + best, _, ok := tr.Nearest([]float64{0, weights[1] * 0.2, weights[2] * 0.3, 0}) + if !ok { + panic("no nearest neighbour found") + } fmt.Println("4D best:", best.ID) } diff --git a/kdtree_helpers.go b/kdtree_helpers.go index 063f0ae..9d16a78 100644 --- a/kdtree_helpers.go +++ b/kdtree_helpers.go @@ -246,7 +246,7 @@ func Build2DWithStats[T any](items []T, id func(T) string, f1, f2 func(T) float6 return nil, nil } if len(stats.Stats) != 2 { - return nil, nil + return nil, ErrStatsDimMismatch } pts := make([]KDPoint[T], len(items)) for i, it := range items { @@ -317,7 +317,7 @@ func Build3DWithStats[T any](items []T, id func(T) string, f1, f2, f3 func(T) fl return nil, nil } if len(stats.Stats) != 3 { - return nil, nil + return nil, ErrStatsDimMismatch } pts := make([]KDPoint[T], len(items)) for i, it := range items { @@ -400,7 +400,7 @@ func Build4DWithStats[T any](items []T, id func(T) string, f1, f2, f3, f4 func(T return nil, nil } if len(stats.Stats) != 4 { - return nil, nil + return nil, ErrStatsDimMismatch } pts := make([]KDPoint[T], len(items)) for i, it := range items { diff --git a/kdtree_morecov_test.go b/kdtree_morecov_test.go index 3cb69db..d7c5073 100644 --- a/kdtree_morecov_test.go +++ b/kdtree_morecov_test.go @@ -48,8 +48,8 @@ func TestDeleteByID_SwapDelete(t *testing.T) { if ids["B"] { t.Fatalf("B should not be present after delete") } - if !ids["A"] && !ids["C"] { - t.Fatalf("expected either A or C to be nearest for respective queries: %v", ids) + if !ids["A"] || !ids["C"] { + t.Fatalf("expected both A and C to be found after deleting B, got: %v", ids) } } diff --git a/npm/poindexter-wasm/loader.js b/npm/poindexter-wasm/loader.js index e58179b..a45c990 100644 --- a/npm/poindexter-wasm/loader.js +++ b/npm/poindexter-wasm/loader.js @@ -74,7 +74,8 @@ export async function init(options = {}) { } // Run the Go program (it registers globals like pxNewTree, etc.) - await go.run(result.instance); + // Do not await: the Go WASM main may block (e.g., via select{}), so awaiting never resolves. + go.run(result.instance); const api = { version: async () => call('pxVersion'),