Refactor CI configuration and documentation; improve error handling in KDTree functions

This commit is contained in:
Snider 2025-11-04 00:38:18 +00:00
parent 8e62b4e51d
commit 5d1ee3f0ea
17 changed files with 65 additions and 28 deletions

View file

@ -8,6 +8,7 @@ on:
permissions:
contents: read
actions: write
jobs:
build-test-wasm:

View file

@ -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.

View file

@ -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.

View file

@ -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`

View file

@ -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)

View file

@ -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,

View file

@ -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.

View file

@ -123,5 +123,5 @@ func main() {
- Check out the [API Reference](api.md) for detailed documentation
- Try the example: [Find the best (lowestping) 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)

View file

@ -237,23 +237,32 @@ func main() {
// Initial 2D 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])

View file

@ -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-<go-version>.txt` in the CI run.
GitHub Actions publishes benchmark artifacts for Go 1.23 on every push/PR. Look for artifacts named `bench-<go-version>.txt` in the CI run.
## What to expect (rule of thumb)

View file

@ -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

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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'),