Compare commits

..

2 commits

Author SHA1 Message Date
544a3bf5ad Merge pull request 'feat: Add math modules — stats, scale, epsilon, score, signal' (#1) from host-uk/Poindexter:feat/math-modules into main
Some checks failed
CI / build-test-wasm (push) Has been cancelled
CI / build-test-gonum (push) Has been cancelled
Deploy Documentation / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
Reviewed-on: #1
2026-02-16 16:19:30 +00:00
Claude
fa998619dc
feat: Add math modules — stats, scale, epsilon, score, signal
Some checks failed
CI / build-test-wasm (pull_request) Has been cancelled
CI / build-test-gonum (pull_request) Has been cancelled
Tests / test (pull_request) Has been cancelled
Centralizes common math operations used across core/go-ai, core/go,
and core/mining into Poindexter as the math pillar (alongside Borg=data,
Enchantrix=encryption).

New modules:
- stats: Sum, Mean, Variance, StdDev, MinMax, IsUnderrepresented
- scale: Lerp, InverseLerp, Remap, RoundToN, Clamp, MinMaxScale
- epsilon: ApproxEqual, ApproxZero
- score: WeightedScore, Ratio, Delta, DeltaPercent
- signal: RampUp, SineWave, Oscillate, Noise (seeded RNG)

235 LOC implementation, 509 LOC tests, zero external deps, WASM-safe.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:16:49 +00:00
17 changed files with 895 additions and 324 deletions

View file

@ -1,3 +1,4 @@
version: "2"
run:
timeout: 5m
linters:

View file

@ -1,69 +0,0 @@
# Audit: Error Handling & Logging
This audit reviews the error handling and logging practices of the Poindexter Go library, focusing on the core `kdtree.go` implementation and the `wasm/main.go` wrapper.
## Error Handling
### Exception Handling
- [x] **Are exceptions caught appropriately?**
- **Finding:** Yes. As a Go library, it doesn't use exceptions but instead returns `error` types. The code diligently checks for and propagates errors. For instance, `NewKDTree` returns an error for invalid input, and callers are expected to handle it.
- [x] **Generic catches hiding bugs?**
- **Finding:** No. The library defines specific, exported error variables (e.g., `ErrEmptyPoints`, `ErrDimMismatch`) in `kdtree.go`. This allows consumers to programmatically check for specific error conditions using `errors.Is`, which is a best practice.
- [x] **Error information leakage?**
- **Finding:** Previously, the WASM wrapper at `wasm/main.go` would return raw Go error strings to the JavaScript client. This has been **remediated** by introducing a structured `WasmError` type with a `code` and `message`, preventing the leakage of internal implementation details.
### Error Recovery
- [x] **Graceful degradation?**
- **Finding:** Yes. The `KDTree` constructor attempts to use the `gonum` backend if requested, but it gracefully falls back to the `linear` backend if the `gonum` build tag is not present or if the backend fails to initialize. This ensures the library remains functional even without the optimized backend.
- [ ] **Retry logic with backoff?**
- **Finding:** Not applicable. This is a computational library, not a networked service, so retry logic is not relevant.
- [ ] **Circuit breaker patterns?**
- **Finding:** Not applicable. This is not a networked service.
### User-Facing Errors
- [x] **Helpful without exposing internals?**
- **Finding:** Yes. The error messages are clear and actionable for developers (e.g., "inconsistent dimensionality in points") without revealing sensitive internal state.
- [x] **Consistent error format?**
- **Finding:** Yes. The Go API uses the standard `error` interface. The WASM API has been updated to use a consistent JSON structure for all errors: `{ "ok": false, "error": { "code": "...", "message": "..." } }`.
- [ ] **Localization support?**
- **Finding:** No. Error messages are in English. For a developer-facing library, this is generally acceptable and localization is not expected.
### API Errors
- [x] **Standard error response format?**
- **Finding:** Yes. As noted above, the WASM API now has a standardized JSON error format.
- [ ] **Appropriate HTTP status codes?**
- **Finding:** Not applicable. This is a WASM module, not an HTTP service.
- [x] **Error codes for clients?**
- **Finding:** Yes. The WASM API now includes standardized string-based error codes (e.g., `bad_request`, `not_found`), allowing clients to handle different error types programmatically.
## Logging
The library itself does not perform any logging, which is appropriate for its role. It returns errors to the calling application, which is then responsible for its own logging strategy. The library does include analytics and metrics collection (`kdtree_analytics.go`), but this is separate from logging and does not record any sensitive information.
### What is Logged
- [ ] Security events (auth, access)? - **N/A**
- [ ] Errors with context? - **N/A**
- [ ] Performance metrics? - **N/A** (collected, but not logged by the library)
### What Should NOT be Logged
- [ ] Passwords/tokens - **N/A**
- [ ] PII without consent - **N/A**
- [ ] Full credit card numbers - **N/A**
### Log Quality
- [ ] Structured logging (JSON)? - **N/A**
- [ ] Correlation IDs? - **N/A**
- [ ] Log levels used correctly? - **N/A**
### Log Security
- [ ] Injection-safe? - **N/A**
- [ ] Tamper-evident? - **N/A**
- [ ] Retention policy? - **N/A**

View file

@ -0,0 +1,46 @@
# Poindexter Math Expansion
**Date:** 2026-02-16
**Status:** Approved
## Context
Poindexter serves as the math pillar (alongside Borg=data, Enchantrix=encryption) in the Lethean ecosystem. It currently provides KD-Tree spatial queries, 5 distance metrics, sorting utilities, and normalization helpers.
Analysis of math operations scattered across core/go, core/go-ai, and core/mining revealed common patterns that Poindexter should centralize: descriptive statistics, scaling/interpolation, approximate equality, weighted scoring, and signal generation.
## New Modules
### stats.go — Descriptive statistics
Sum, Mean, Variance, StdDev, MinMax, IsUnderrepresented.
Consumers: ml/coverage.go, lab/handler/chart.go
### scale.go — Normalization and interpolation
Lerp, InverseLerp, Remap, RoundToN, Clamp, MinMaxScale.
Consumers: lab/handler/chart.go, i18n/numbers.go
### epsilon.go — Approximate equality
ApproxEqual, ApproxZero.
Consumers: ml/exact.go
### score.go — Weighted composite scoring
Factor type, WeightedScore, Ratio, Delta, DeltaPercent.
Consumers: ml/heuristic.go, ml/compare.go
### signal.go — Time-series primitives
RampUp, SineWave, Oscillate, Noise (seeded RNG).
Consumers: mining/simulated_miner.go
## Constraints
- Zero external dependencies (WASM-compilable)
- Pure Go, stdlib only (math, math/rand)
- Same package (`poindexter`), flat structure
- Table-driven tests for every function
- No changes to existing files
## Not In Scope
- MLX tensor ops (hardware-accelerated, stays in go-ai)
- DNS tools migration to go-netops (separate PR)
- gonum backend integration (future work)

14
epsilon.go Normal file
View file

@ -0,0 +1,14 @@
package poindexter
import "math"
// ApproxEqual returns true if the absolute difference between a and b
// is less than epsilon.
func ApproxEqual(a, b, epsilon float64) bool {
return math.Abs(a-b) < epsilon
}
// ApproxZero returns true if the absolute value of v is less than epsilon.
func ApproxZero(v, epsilon float64) bool {
return math.Abs(v) < epsilon
}

50
epsilon_test.go Normal file
View file

@ -0,0 +1,50 @@
package poindexter
import "testing"
func TestApproxEqual(t *testing.T) {
tests := []struct {
name string
a, b float64
epsilon float64
want bool
}{
{"equal", 1.0, 1.0, 0.01, true},
{"close", 1.0, 1.005, 0.01, true},
{"not_close", 1.0, 1.02, 0.01, false},
{"negative", -1.0, -1.005, 0.01, true},
{"zero", 0, 0.0001, 0.001, true},
{"at_boundary", 1.0, 1.01, 0.01, false},
{"large_epsilon", 100, 200, 150, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ApproxEqual(tt.a, tt.b, tt.epsilon)
if got != tt.want {
t.Errorf("ApproxEqual(%v, %v, %v) = %v, want %v", tt.a, tt.b, tt.epsilon, got, tt.want)
}
})
}
}
func TestApproxZero(t *testing.T) {
tests := []struct {
name string
v float64
epsilon float64
want bool
}{
{"zero", 0, 0.01, true},
{"small_pos", 0.005, 0.01, true},
{"small_neg", -0.005, 0.01, true},
{"not_zero", 0.02, 0.01, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ApproxZero(tt.v, tt.epsilon)
if got != tt.want {
t.Errorf("ApproxZero(%v, %v) = %v, want %v", tt.v, tt.epsilon, got, tt.want)
}
})
}
}

View file

@ -71,9 +71,7 @@ Explore runnable examples in the repository:
- examples/kdtree_2d_ping_hop
- examples/kdtree_3d_ping_hop_geo
- examples/kdtree_4d_ping_hop_geo_score
- examples/dht_helpers (convenience wrappers for common DHT schemas)
- examples/wasm-browser (browser demo using the ESM loader)
- examples/wasm-browser-ts (TypeScript + Vite local demo)
### KDTree performance and notes
- Dual backend support: Linear (always available) and an optimized KD backend enabled when building with `-tags=gonum`. Linear is the default; with the `gonum` tag, the optimized backend becomes the default.
@ -218,21 +216,4 @@ This project is licensed under the European Union Public Licence v1.2 (EUPL-1.2)
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Coverage
- CI produces coverage summaries as artifacts on every push/PR:
- Default job: `coverage-summary.md` (from `coverage.out`)
- Gonum-tag job: `coverage-summary-gonum.md` (from `coverage-gonum.out`)
- Locally, you can generate and inspect coverage with the Makefile:
```bash
make cover # runs tests with race + coverage and prints the total
make coverfunc # prints per-function coverage
make cover-kdtree # filters coverage to kdtree.go
make coverhtml # writes coverage.html for visual inspection
```
Note: CI also uploads raw coverage profiles as artifacts (`coverage.out`, `coverage-gonum.out`).
Contributions are welcome! Please feel free to submit a Pull Request.

View file

@ -6,11 +6,6 @@
// await tree.insert({ id: 'a', coords: [0,0], value: 'A' });
// const res = await tree.nearest([0.1, 0.2]);
// --- Environment detection ---
const isBrowser = typeof window !== 'undefined';
const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
// --- Browser-specific helpers ---
async function loadScriptOnce(src) {
return new Promise((resolve, reject) => {
// If already present, resolve immediately
@ -23,44 +18,20 @@ async function loadScriptOnce(src) {
});
}
// --- Loader logic ---
async function ensureWasmExec(url) {
if (typeof globalThis.Go === 'function') return;
if (isBrowser) {
await loadScriptOnce(url);
} else if (isNode) {
const { fileURLToPath } = await import('url');
const wasmExecPath = fileURLToPath(url);
await import(wasmExecPath);
} else {
throw new Error(`Unsupported environment: cannot load ${url}`);
}
if (typeof globalThis.Go !== 'function') {
throw new Error('wasm_exec.js did not define globalThis.Go');
if (typeof window !== 'undefined' && typeof window.Go === 'function') return;
await loadScriptOnce(url);
if (typeof window === 'undefined' || typeof window.Go !== 'function') {
throw new Error('wasm_exec.js did not define window.Go');
}
}
function unwrap(result) {
if (!result || typeof result !== 'object') {
throw new Error(`bad/unexpected result type from WASM: ${typeof result}`);
}
if (result.ok) {
return result.data;
}
// Handle structured errors, which may be nested
const errorPayload = result.error || result;
if (errorPayload && typeof errorPayload === 'object') {
const err = new Error(errorPayload.message || 'unknown WASM error');
err.code = errorPayload.code;
throw err;
}
// Fallback for simple string errors
throw new Error(errorPayload || 'unknown WASM error');
if (!result || typeof result !== 'object') throw new Error('bad result');
if (result.ok) return result.data;
throw new Error(result.error || 'unknown error');
}
function call(name, ...args) {
const fn = globalThis[name];
if (typeof fn !== 'function') throw new Error(`WASM function ${name} not found`);
@ -94,32 +65,18 @@ export async function init(options = {}) {
} = options;
await ensureWasmExec(wasmExecURL);
const go = new globalThis.Go();
const go = new window.Go();
let result;
if (instantiateWasm) {
let source;
if (isBrowser) {
source = await fetch(wasmURL).then(r => r.arrayBuffer());
} else {
const fs = await import('fs/promises');
const { fileURLToPath } = await import('url');
source = await fs.readFile(fileURLToPath(wasmURL));
}
const source = await fetch(wasmURL).then(r => r.arrayBuffer());
const inst = await instantiateWasm(source, go.importObject);
result = { instance: inst };
} else if (isBrowser && WebAssembly.instantiateStreaming) {
} else if (WebAssembly.instantiateStreaming) {
result = await WebAssembly.instantiateStreaming(fetch(wasmURL), go.importObject);
} else {
let bytes;
if (isBrowser) {
const resp = await fetch(wasmURL);
bytes = await resp.arrayBuffer();
} else {
const fs = await import('fs/promises');
const { fileURLToPath } = await import('url');
bytes = await fs.readFile(fileURLToPath(wasmURL));
}
const resp = await fetch(wasmURL);
const bytes = await resp.arrayBuffer();
result = await WebAssembly.instantiate(bytes, go.importObject);
}

View file

@ -4,11 +4,11 @@
import { init } from './loader.js';
(async function () {
let px;
try {
px = await init({
wasmURL: new URL('./dist/poindexter.wasm', import.meta.url).toString(),
wasmExecURL: new URL('./dist/wasm_exec.js', import.meta.url).toString(),
const px = await init({
// In CI, dist/ is placed at repo root via make wasm-build && make npm-pack
wasmURL: new URL('./dist/poindexter.wasm', import.meta.url).pathname,
wasmExecURL: new URL('./dist/wasm_exec.js', import.meta.url).pathname,
});
const ver = await px.version();
if (!ver || typeof ver !== 'string') throw new Error('version not string');
@ -17,24 +17,10 @@ import { init } from './loader.js';
await tree.insert({ id: 'a', coords: [0, 0], value: 'A' });
await tree.insert({ id: 'b', coords: [1, 0], value: 'B' });
const nn = await tree.nearest([0.9, 0.1]);
if (!nn || !nn.point || !nn.point.id) throw new Error('nearest failed');
console.log('WASM smoke ok:', ver, 'nearest.id=', nn.point.id);
if (!nn || !nn.id) throw new Error('nearest failed');
console.log('WASM smoke ok:', ver, 'nearest.id=', nn.id);
} catch (err) {
console.error('WASM smoke failed:', err);
process.exit(1);
}
// Test error handling
try {
await px.newTree(0);
console.error('Expected error from newTree(0) but got none');
process.exit(1);
} catch (err) {
if (err.code === 'bad_request' && err.message.includes('dimension')) {
console.log('WASM error handling ok:', err.code);
} else {
console.error('WASM smoke failed: unexpected error format:', err);
process.exit(1);
}
}
})();

61
scale.go Normal file
View file

@ -0,0 +1,61 @@
package poindexter
import "math"
// Lerp performs linear interpolation between a and b.
// t=0 returns a, t=1 returns b, t=0.5 returns the midpoint.
func Lerp(t, a, b float64) float64 {
return a + t*(b-a)
}
// InverseLerp returns where v falls between a and b as a fraction [0,1].
// Returns 0 if a == b.
func InverseLerp(v, a, b float64) float64 {
if a == b {
return 0
}
return (v - a) / (b - a)
}
// Remap maps v from the range [inMin, inMax] to [outMin, outMax].
// Equivalent to Lerp(InverseLerp(v, inMin, inMax), outMin, outMax).
func Remap(v, inMin, inMax, outMin, outMax float64) float64 {
return Lerp(InverseLerp(v, inMin, inMax), outMin, outMax)
}
// RoundToN rounds f to n decimal places.
func RoundToN(f float64, decimals int) float64 {
mul := math.Pow(10, float64(decimals))
return math.Round(f*mul) / mul
}
// Clamp restricts v to the range [min, max].
func Clamp(v, min, max float64) float64 {
if v < min {
return min
}
if v > max {
return max
}
return v
}
// ClampInt restricts v to the range [min, max].
func ClampInt(v, min, max int) int {
if v < min {
return min
}
if v > max {
return max
}
return v
}
// MinMaxScale normalizes v into [0,1] given its range [min, max].
// Returns 0 if min == max.
func MinMaxScale(v, min, max float64) float64 {
if min == max {
return 0
}
return (v - min) / (max - min)
}

148
scale_test.go Normal file
View file

@ -0,0 +1,148 @@
package poindexter
import (
"math"
"testing"
)
func TestLerp(t *testing.T) {
tests := []struct {
name string
t_, a, b float64
want float64
}{
{"start", 0, 10, 20, 10},
{"end", 1, 10, 20, 20},
{"mid", 0.5, 10, 20, 15},
{"quarter", 0.25, 0, 100, 25},
{"extrapolate", 2, 0, 10, 20},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Lerp(tt.t_, tt.a, tt.b)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("Lerp(%v, %v, %v) = %v, want %v", tt.t_, tt.a, tt.b, got, tt.want)
}
})
}
}
func TestInverseLerp(t *testing.T) {
tests := []struct {
name string
v, a, b float64
want float64
}{
{"start", 10, 10, 20, 0},
{"end", 20, 10, 20, 1},
{"mid", 15, 10, 20, 0.5},
{"equal_range", 5, 5, 5, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := InverseLerp(tt.v, tt.a, tt.b)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("InverseLerp(%v, %v, %v) = %v, want %v", tt.v, tt.a, tt.b, got, tt.want)
}
})
}
}
func TestRemap(t *testing.T) {
tests := []struct {
name string
v, inMin, inMax, outMin, outMax float64
want float64
}{
{"identity", 5, 0, 10, 0, 10, 5},
{"scale_up", 5, 0, 10, 0, 100, 50},
{"reverse", 3, 0, 10, 10, 0, 7},
{"offset", 0, 0, 1, 100, 200, 100},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Remap(tt.v, tt.inMin, tt.inMax, tt.outMin, tt.outMax)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("Remap = %v, want %v", got, tt.want)
}
})
}
}
func TestRoundToN(t *testing.T) {
tests := []struct {
name string
f float64
decimals int
want float64
}{
{"zero_dec", 3.456, 0, 3},
{"one_dec", 3.456, 1, 3.5},
{"two_dec", 3.456, 2, 3.46},
{"three_dec", 3.4564, 3, 3.456},
{"negative", -2.555, 2, -2.56},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := RoundToN(tt.f, tt.decimals)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("RoundToN(%v, %v) = %v, want %v", tt.f, tt.decimals, got, tt.want)
}
})
}
}
func TestClamp(t *testing.T) {
tests := []struct {
name string
v, min, max float64
want float64
}{
{"within", 5, 0, 10, 5},
{"below", -5, 0, 10, 0},
{"above", 15, 0, 10, 10},
{"at_min", 0, 0, 10, 0},
{"at_max", 10, 0, 10, 10},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Clamp(tt.v, tt.min, tt.max)
if got != tt.want {
t.Errorf("Clamp(%v, %v, %v) = %v, want %v", tt.v, tt.min, tt.max, got, tt.want)
}
})
}
}
func TestClampInt(t *testing.T) {
if got := ClampInt(5, 0, 10); got != 5 {
t.Errorf("ClampInt(5, 0, 10) = %v, want 5", got)
}
if got := ClampInt(-1, 0, 10); got != 0 {
t.Errorf("ClampInt(-1, 0, 10) = %v, want 0", got)
}
if got := ClampInt(15, 0, 10); got != 10 {
t.Errorf("ClampInt(15, 0, 10) = %v, want 10", got)
}
}
func TestMinMaxScale(t *testing.T) {
tests := []struct {
name string
v, min, max float64
want float64
}{
{"mid", 5, 0, 10, 0.5},
{"at_min", 0, 0, 10, 0},
{"at_max", 10, 0, 10, 1},
{"equal_range", 5, 5, 5, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := MinMaxScale(tt.v, tt.min, tt.max)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("MinMaxScale(%v, %v, %v) = %v, want %v", tt.v, tt.min, tt.max, got, tt.want)
}
})
}
}

40
score.go Normal file
View file

@ -0,0 +1,40 @@
package poindexter
// Factor is a valueweight pair for composite scoring.
type Factor struct {
Value float64
Weight float64
}
// WeightedScore computes the weighted sum of factors.
// Each factor contributes Value * Weight to the total.
// Returns 0 for empty slices.
func WeightedScore(factors []Factor) float64 {
var total float64
for _, f := range factors {
total += f.Value * f.Weight
}
return total
}
// Ratio returns part/whole safely. Returns 0 if whole is 0.
func Ratio(part, whole float64) float64 {
if whole == 0 {
return 0
}
return part / whole
}
// Delta returns the difference new_ - old.
func Delta(old, new_ float64) float64 {
return new_ - old
}
// DeltaPercent returns the percentage change from old to new_.
// Returns 0 if old is 0.
func DeltaPercent(old, new_ float64) float64 {
if old == 0 {
return 0
}
return (new_ - old) / old * 100
}

86
score_test.go Normal file
View file

@ -0,0 +1,86 @@
package poindexter
import (
"math"
"testing"
)
func TestWeightedScore(t *testing.T) {
tests := []struct {
name string
factors []Factor
want float64
}{
{"empty", nil, 0},
{"single", []Factor{{Value: 5, Weight: 2}}, 10},
{"multiple", []Factor{
{Value: 3, Weight: 2}, // 6
{Value: 1, Weight: -5}, // -5
}, 1},
{"lek_heuristic", []Factor{
{Value: 2, Weight: 2}, // engagement × 2
{Value: 1, Weight: 3}, // creative × 3
{Value: 1, Weight: 1.5}, // first person × 1.5
{Value: 3, Weight: -5}, // compliance × -5
}, -6.5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := WeightedScore(tt.factors)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("WeightedScore = %v, want %v", got, tt.want)
}
})
}
}
func TestRatio(t *testing.T) {
tests := []struct {
name string
part, whole float64
want float64
}{
{"half", 5, 10, 0.5},
{"full", 10, 10, 1},
{"zero_whole", 5, 0, 0},
{"zero_part", 0, 10, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Ratio(tt.part, tt.whole)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("Ratio(%v, %v) = %v, want %v", tt.part, tt.whole, got, tt.want)
}
})
}
}
func TestDelta(t *testing.T) {
if got := Delta(10, 15); got != 5 {
t.Errorf("Delta(10, 15) = %v, want 5", got)
}
if got := Delta(15, 10); got != -5 {
t.Errorf("Delta(15, 10) = %v, want -5", got)
}
}
func TestDeltaPercent(t *testing.T) {
tests := []struct {
name string
old, new_ float64
want float64
}{
{"increase", 100, 150, 50},
{"decrease", 100, 75, -25},
{"zero_old", 0, 10, 0},
{"no_change", 50, 50, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := DeltaPercent(tt.old, tt.new_)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("DeltaPercent(%v, %v) = %v, want %v", tt.old, tt.new_, got, tt.want)
}
})
}
}

57
signal.go Normal file
View file

@ -0,0 +1,57 @@
package poindexter
import (
"math"
"math/rand"
)
// RampUp returns a linear ramp from 0 to 1 over the given duration.
// The result is clamped to [0, 1].
func RampUp(elapsed, duration float64) float64 {
if duration <= 0 {
return 1
}
return Clamp(elapsed/duration, 0, 1)
}
// SineWave returns a sine value with the given period and amplitude.
// Output range is [-amplitude, amplitude].
func SineWave(t, period, amplitude float64) float64 {
if period == 0 {
return 0
}
return math.Sin(t/period*2*math.Pi) * amplitude
}
// Oscillate modulates a base value with a sine wave.
// Returns base * (1 + sin(t/period*2π) * amplitude).
func Oscillate(base, t, period, amplitude float64) float64 {
if period == 0 {
return base
}
return base * (1 + math.Sin(t/period*2*math.Pi)*amplitude)
}
// Noise generates seeded pseudo-random values.
type Noise struct {
rng *rand.Rand
}
// NewNoise creates a seeded noise generator.
func NewNoise(seed int64) *Noise {
return &Noise{rng: rand.New(rand.NewSource(seed))}
}
// Float64 returns a random value in [-variance, variance].
func (n *Noise) Float64(variance float64) float64 {
return (n.rng.Float64() - 0.5) * 2 * variance
}
// Int returns a random integer in [0, max).
// Returns 0 if max <= 0.
func (n *Noise) Int(max int) int {
if max <= 0 {
return 0
}
return n.rng.Intn(max)
}

103
signal_test.go Normal file
View file

@ -0,0 +1,103 @@
package poindexter
import (
"math"
"testing"
)
func TestRampUp(t *testing.T) {
tests := []struct {
name string
elapsed, duration float64
want float64
}{
{"start", 0, 30, 0},
{"mid", 15, 30, 0.5},
{"end", 30, 30, 1},
{"over", 60, 30, 1},
{"negative", -5, 30, 0},
{"zero_duration", 10, 0, 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := RampUp(tt.elapsed, tt.duration)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("RampUp(%v, %v) = %v, want %v", tt.elapsed, tt.duration, got, tt.want)
}
})
}
}
func TestSineWave(t *testing.T) {
// At t=0, sin(0) = 0
if got := SineWave(0, 10, 5); math.Abs(got) > 1e-9 {
t.Errorf("SineWave(0, 10, 5) = %v, want 0", got)
}
// At t=period/4, sin(π/2) = 1, so result = amplitude
got := SineWave(2.5, 10, 5)
if math.Abs(got-5) > 1e-9 {
t.Errorf("SineWave(2.5, 10, 5) = %v, want 5", got)
}
// Zero period returns 0
if got := SineWave(5, 0, 5); got != 0 {
t.Errorf("SineWave(5, 0, 5) = %v, want 0", got)
}
}
func TestOscillate(t *testing.T) {
// At t=0, sin(0)=0, so result = base * (1 + 0) = base
got := Oscillate(100, 0, 10, 0.05)
if math.Abs(got-100) > 1e-9 {
t.Errorf("Oscillate(100, 0, 10, 0.05) = %v, want 100", got)
}
// At t=period/4, sin=1, so result = base * (1 + amplitude)
got = Oscillate(100, 2.5, 10, 0.05)
if math.Abs(got-105) > 1e-9 {
t.Errorf("Oscillate(100, 2.5, 10, 0.05) = %v, want 105", got)
}
// Zero period returns base
if got := Oscillate(100, 5, 0, 0.05); got != 100 {
t.Errorf("Oscillate(100, 5, 0, 0.05) = %v, want 100", got)
}
}
func TestNoise(t *testing.T) {
n := NewNoise(42)
// Float64 should be within [-variance, variance]
for i := 0; i < 1000; i++ {
v := n.Float64(0.1)
if v < -0.1 || v > 0.1 {
t.Fatalf("Float64(0.1) = %v, outside [-0.1, 0.1]", v)
}
}
// Int should be within [0, max)
n2 := NewNoise(42)
for i := 0; i < 1000; i++ {
v := n2.Int(10)
if v < 0 || v >= 10 {
t.Fatalf("Int(10) = %v, outside [0, 10)", v)
}
}
// Int with zero max returns 0
if got := n.Int(0); got != 0 {
t.Errorf("Int(0) = %v, want 0", got)
}
if got := n.Int(-1); got != 0 {
t.Errorf("Int(-1) = %v, want 0", got)
}
}
func TestNoiseDeterministic(t *testing.T) {
n1 := NewNoise(123)
n2 := NewNoise(123)
for i := 0; i < 100; i++ {
a := n1.Float64(1.0)
b := n2.Float64(1.0)
if a != b {
t.Fatalf("iteration %d: different values for same seed: %v != %v", i, a, b)
}
}
}

63
stats.go Normal file
View file

@ -0,0 +1,63 @@
package poindexter
import "math"
// Sum returns the sum of all values. Returns 0 for empty slices.
func Sum(data []float64) float64 {
var s float64
for _, v := range data {
s += v
}
return s
}
// Mean returns the arithmetic mean. Returns 0 for empty slices.
func Mean(data []float64) float64 {
if len(data) == 0 {
return 0
}
return Sum(data) / float64(len(data))
}
// Variance returns the population variance. Returns 0 for empty slices.
func Variance(data []float64) float64 {
if len(data) == 0 {
return 0
}
m := Mean(data)
var ss float64
for _, v := range data {
d := v - m
ss += d * d
}
return ss / float64(len(data))
}
// StdDev returns the population standard deviation.
func StdDev(data []float64) float64 {
return math.Sqrt(Variance(data))
}
// MinMax returns the minimum and maximum values.
// Returns (0, 0) for empty slices.
func MinMax(data []float64) (min, max float64) {
if len(data) == 0 {
return 0, 0
}
min, max = data[0], data[0]
for _, v := range data[1:] {
if v < min {
min = v
}
if v > max {
max = v
}
}
return min, max
}
// IsUnderrepresented returns true if val is below threshold fraction of avg.
// For example, IsUnderrepresented(3, 10, 0.5) returns true because 3 < 10*0.5.
func IsUnderrepresented(val, avg, threshold float64) bool {
return val < avg*threshold
}

122
stats_test.go Normal file
View file

@ -0,0 +1,122 @@
package poindexter
import (
"math"
"testing"
)
func TestSum(t *testing.T) {
tests := []struct {
name string
data []float64
want float64
}{
{"empty", nil, 0},
{"single", []float64{5}, 5},
{"multiple", []float64{1, 2, 3, 4, 5}, 15},
{"negative", []float64{-1, -2, 3}, 0},
{"floats", []float64{0.1, 0.2, 0.3}, 0.6},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Sum(tt.data)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("Sum(%v) = %v, want %v", tt.data, got, tt.want)
}
})
}
}
func TestMean(t *testing.T) {
tests := []struct {
name string
data []float64
want float64
}{
{"empty", nil, 0},
{"single", []float64{5}, 5},
{"multiple", []float64{2, 4, 6}, 4},
{"floats", []float64{1.5, 2.5}, 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Mean(tt.data)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("Mean(%v) = %v, want %v", tt.data, got, tt.want)
}
})
}
}
func TestVariance(t *testing.T) {
tests := []struct {
name string
data []float64
want float64
}{
{"empty", nil, 0},
{"constant", []float64{5, 5, 5}, 0},
{"simple", []float64{2, 4, 4, 4, 5, 5, 7, 9}, 4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Variance(tt.data)
if math.Abs(got-tt.want) > 1e-9 {
t.Errorf("Variance(%v) = %v, want %v", tt.data, got, tt.want)
}
})
}
}
func TestStdDev(t *testing.T) {
got := StdDev([]float64{2, 4, 4, 4, 5, 5, 7, 9})
if math.Abs(got-2) > 1e-9 {
t.Errorf("StdDev = %v, want 2", got)
}
}
func TestMinMax(t *testing.T) {
tests := []struct {
name string
data []float64
wantMin float64
wantMax float64
}{
{"empty", nil, 0, 0},
{"single", []float64{3}, 3, 3},
{"ordered", []float64{1, 2, 3}, 1, 3},
{"reversed", []float64{3, 2, 1}, 1, 3},
{"negative", []float64{-5, 0, 5}, -5, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotMin, gotMax := MinMax(tt.data)
if gotMin != tt.wantMin || gotMax != tt.wantMax {
t.Errorf("MinMax(%v) = (%v, %v), want (%v, %v)", tt.data, gotMin, gotMax, tt.wantMin, tt.wantMax)
}
})
}
}
func TestIsUnderrepresented(t *testing.T) {
tests := []struct {
name string
val float64
avg float64
threshold float64
want bool
}{
{"below", 3, 10, 0.5, true},
{"at", 5, 10, 0.5, false},
{"above", 7, 10, 0.5, false},
{"zero_avg", 0, 0, 0.5, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsUnderrepresented(tt.val, tt.avg, tt.threshold)
if got != tt.want {
t.Errorf("IsUnderrepresented(%v, %v, %v) = %v, want %v", tt.val, tt.avg, tt.threshold, got, tt.want)
}
})
}
}

View file

@ -18,62 +18,26 @@ var (
nextTreeID = 1
)
// WasmError provides a structured error for WASM responses.
type WasmError struct {
Code string `json:"code"`
Message string `json:"message"`
}
func (e *WasmError) Error() string {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
// Standard error codes.
const (
ErrCodeBadRequest = "bad_request"
ErrCodeNotFound = "not_found"
ErrCodeConflict = "conflict"
)
func newErr(code, msg string) *WasmError {
return &WasmError{Code: code, Message: msg}
}
func newErrf(code, format string, args ...any) *WasmError {
return &WasmError{Code: code, Message: fmt.Sprintf(format, args...)}
}
func export(name string, fn func(this js.Value, args []js.Value) (any, error)) {
js.Global().Set(name, js.FuncOf(func(this js.Value, args []js.Value) any {
res, err := fn(this, args)
if err != nil {
var wasmErr *WasmError
if errors.As(err, &wasmErr) {
return map[string]any{
"ok": false,
"error": map[string]any{"code": wasmErr.Code, "message": wasmErr.Message},
}
}
// Fallback for generic errors
return map[string]any{
"ok": false,
"error": map[string]any{"code": ErrCodeBadRequest, "message": err.Error()},
}
return map[string]any{"ok": false, "error": err.Error()}
}
return map[string]any{"ok": true, "data": res}
}))
}
func getInt(args []js.Value, idx int, name string) (int, error) {
if len(args) > idx {
return args[idx].Int(), nil
func getInt(v js.Value, idx int) (int, error) {
if len := v.Length(); len > idx {
return v.Index(idx).Int(), nil
}
return 0, newErrf(ErrCodeBadRequest, "missing integer argument: %s", name)
return 0, errors.New("missing integer argument")
}
func getFloatSlice(arg js.Value, name string) ([]float64, error) {
func getFloatSlice(arg js.Value) ([]float64, error) {
if arg.IsUndefined() || arg.IsNull() {
return nil, newErrf(ErrCodeBadRequest, "argument is undefined or null: %s", name)
return nil, errors.New("coords/query is undefined or null")
}
ln := arg.Length()
res := make([]float64, ln)
@ -83,19 +47,6 @@ func getFloatSlice(arg js.Value, name string) ([]float64, error) {
return res, nil
}
// pointToJS converts a KDPoint to a JS-friendly map, ensuring slices are []any.
func pointToJS(p pd.KDPoint[string]) map[string]any {
coords := make([]any, len(p.Coords))
for i, c := range p.Coords {
coords[i] = c
}
return map[string]any{
"id": p.ID,
"coords": coords,
"value": p.Value,
}
}
func version(_ js.Value, _ []js.Value) (any, error) {
return pd.Version(), nil
}
@ -110,15 +61,15 @@ func hello(_ js.Value, args []js.Value) (any, error) {
func newTree(_ js.Value, args []js.Value) (any, error) {
if len(args) < 1 {
return nil, newErr(ErrCodeBadRequest, "newTree(dim) requires 'dim' argument")
return nil, errors.New("newTree(dim) requires dim")
}
dim := args[0].Int()
if dim <= 0 {
return nil, newErr(ErrCodeBadRequest, pd.ErrZeroDim.Error())
return nil, pd.ErrZeroDim
}
t, err := pd.NewKDTreeFromDim[string](dim)
if err != nil {
return nil, newErr(ErrCodeBadRequest, err.Error())
return nil, err
}
id := nextTreeID
nextTreeID++
@ -127,95 +78,81 @@ func newTree(_ js.Value, args []js.Value) (any, error) {
}
func treeLen(_ js.Value, args []js.Value) (any, error) {
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
if len(args) < 1 {
return nil, errors.New("len(treeId)")
}
id := args[0].Int()
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
return t.Len(), nil
}
func treeDim(_ js.Value, args []js.Value) (any, error) {
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
if len(args) < 1 {
return nil, errors.New("dim(treeId)")
}
id := args[0].Int()
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
return t.Dim(), nil
}
func insert(_ js.Value, args []js.Value) (any, error) {
// insert(treeId, {id: string, coords: number[], value?: string})
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
}
if len(args) < 2 {
return nil, newErr(ErrCodeBadRequest, "insert(treeId, point) requires 'point' argument")
return nil, errors.New("insert(treeId, point)")
}
id := args[0].Int()
pt := args[1]
pid := pt.Get("id").String()
coords, err := getFloatSlice(pt.Get("coords"), "point.coords")
coords, err := getFloatSlice(pt.Get("coords"))
if err != nil {
return nil, err
}
val := pt.Get("value").String()
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
if okIns := t.Insert(pd.KDPoint[string]{ID: pid, Coords: coords, Value: val}); !okIns {
return nil, newErr(ErrCodeConflict, "failed to insert point: dimension mismatch or duplicate ID")
}
return true, nil
okIns := t.Insert(pd.KDPoint[string]{ID: pid, Coords: coords, Value: val})
return okIns, nil
}
func deleteByID(_ js.Value, args []js.Value) (any, error) {
// deleteByID(treeId, id)
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
}
if len(args) < 2 {
return nil, newErr(ErrCodeBadRequest, "deleteByID(treeId, id) requires 'id' argument")
return nil, errors.New("deleteByID(treeId, id)")
}
id := args[0].Int()
pid := args[1].String()
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
if !t.DeleteByID(pid) {
return nil, newErrf(ErrCodeNotFound, "point with id '%s' not found", pid)
}
return true, nil
return t.DeleteByID(pid), nil
}
func nearest(_ js.Value, args []js.Value) (any, error) {
// nearest(treeId, query:number[]) -> {point, dist, found}
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
}
if len(args) < 2 {
return nil, newErr(ErrCodeBadRequest, "nearest(treeId, query) requires 'query' argument")
return nil, errors.New("nearest(treeId, query)")
}
query, err := getFloatSlice(args[1], "query")
id := args[0].Int()
query, err := getFloatSlice(args[1])
if err != nil {
return nil, err
}
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
p, d, found := t.Nearest(query)
out := map[string]any{
"point": pointToJS(p),
"point": map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value},
"dist": d,
"found": found,
}
@ -224,74 +161,65 @@ func nearest(_ js.Value, args []js.Value) (any, error) {
func kNearest(_ js.Value, args []js.Value) (any, error) {
// kNearest(treeId, query:number[], k:int) -> {points:[...], dists:[...]}
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
}
if len(args) < 3 {
return nil, newErr(ErrCodeBadRequest, "kNearest(treeId, query, k) requires 'query' and 'k' arguments")
return nil, errors.New("kNearest(treeId, query, k)")
}
query, err := getFloatSlice(args[1], "query")
if err != nil {
return nil, err
}
k, err := getInt(args, 2, "k")
id := args[0].Int()
query, err := getFloatSlice(args[1])
if err != nil {
return nil, err
}
k := args[2].Int()
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
pts, dists := t.KNearest(query, k)
jsPts := make([]any, len(pts))
for i, p := range pts {
jsPts[i] = pointToJS(p)
jsPts[i] = map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value}
}
return map[string]any{"points": jsPts, "dists": dists}, nil
}
func radius(_ js.Value, args []js.Value) (any, error) {
// radius(treeId, query:number[], r:number) -> {points:[...], dists:[...]}
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
}
if len(args) < 3 {
return nil, newErr(ErrCodeBadRequest, "radius(treeId, query, r) requires 'query' and 'r' arguments")
return nil, errors.New("radius(treeId, query, r)")
}
query, err := getFloatSlice(args[1], "query")
id := args[0].Int()
query, err := getFloatSlice(args[1])
if err != nil {
return nil, err
}
r := args[2].Float()
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
pts, dists := t.Radius(query, r)
jsPts := make([]any, len(pts))
for i, p := range pts {
jsPts[i] = pointToJS(p)
jsPts[i] = map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value}
}
return map[string]any{"points": jsPts, "dists": dists}, nil
}
func exportJSON(_ js.Value, args []js.Value) (any, error) {
// exportJSON(treeId) -> string (all points)
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
if len(args) < 1 {
return nil, errors.New("exportJSON(treeId)")
}
id := args[0].Int()
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
// Export all points
points := t.Points()
jsPts := make([]any, len(points))
for i, p := range points {
jsPts[i] = pointToJS(p)
jsPts[i] = map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value}
}
m := map[string]any{
"dim": t.Dim(),
@ -305,13 +233,13 @@ func exportJSON(_ js.Value, args []js.Value) (any, error) {
func getAnalytics(_ js.Value, args []js.Value) (any, error) {
// getAnalytics(treeId) -> analytics snapshot
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
if len(args) < 1 {
return nil, errors.New("getAnalytics(treeId)")
}
id := args[0].Int()
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
snap := t.GetAnalyticsSnapshot()
return map[string]any{
@ -331,13 +259,13 @@ func getAnalytics(_ js.Value, args []js.Value) (any, error) {
func getPeerStats(_ js.Value, args []js.Value) (any, error) {
// getPeerStats(treeId) -> array of peer stats
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
if len(args) < 1 {
return nil, errors.New("getPeerStats(treeId)")
}
id := args[0].Int()
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
stats := t.GetPeerStats()
jsStats := make([]any, len(stats))
@ -354,17 +282,14 @@ func getPeerStats(_ js.Value, args []js.Value) (any, error) {
func getTopPeers(_ js.Value, args []js.Value) (any, error) {
// getTopPeers(treeId, n) -> array of top n peer stats
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
}
n, err := getInt(args, 1, "n")
if err != nil {
return nil, err
if len(args) < 2 {
return nil, errors.New("getTopPeers(treeId, n)")
}
id := args[0].Int()
n := args[1].Int()
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
stats := t.GetTopPeers(n)
jsStats := make([]any, len(stats))
@ -381,13 +306,13 @@ func getTopPeers(_ js.Value, args []js.Value) (any, error) {
func getAxisDistributions(_ js.Value, args []js.Value) (any, error) {
// getAxisDistributions(treeId, axisNames?: string[]) -> array of axis distribution stats
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
if len(args) < 1 {
return nil, errors.New("getAxisDistributions(treeId)")
}
id := args[0].Int()
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
var axisNames []string
@ -426,13 +351,13 @@ func getAxisDistributions(_ js.Value, args []js.Value) (any, error) {
func resetAnalytics(_ js.Value, args []js.Value) (any, error) {
// resetAnalytics(treeId) -> resets all analytics
id, err := getInt(args, 0, "treeId")
if err != nil {
return nil, err
if len(args) < 1 {
return nil, errors.New("resetAnalytics(treeId)")
}
id := args[0].Int()
t, ok := treeRegistry[id]
if !ok {
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
return nil, fmt.Errorf("unknown treeId %d", id)
}
t.ResetAnalytics()
return true, nil
@ -441,9 +366,9 @@ func resetAnalytics(_ js.Value, args []js.Value) (any, error) {
func computeDistributionStats(_ js.Value, args []js.Value) (any, error) {
// computeDistributionStats(distances: number[]) -> distribution stats
if len(args) < 1 {
return nil, newErr(ErrCodeBadRequest, "computeDistributionStats(distances) requires 'distances' argument")
return nil, errors.New("computeDistributionStats(distances)")
}
distances, err := getFloatSlice(args[0], "distances")
distances, err := getFloatSlice(args[0])
if err != nil {
return nil, err
}
@ -469,7 +394,7 @@ func computeDistributionStats(_ js.Value, args []js.Value) (any, error) {
func computePeerQualityScore(_ js.Value, args []js.Value) (any, error) {
// computePeerQualityScore(metrics: NATRoutingMetrics, weights?: QualityWeights) -> score
if len(args) < 1 {
return nil, newErr(ErrCodeBadRequest, "computePeerQualityScore(metrics) requires 'metrics' argument")
return nil, errors.New("computePeerQualityScore(metrics)")
}
m := args[0]
metrics := pd.NATRoutingMetrics{
@ -507,7 +432,7 @@ func computePeerQualityScore(_ js.Value, args []js.Value) (any, error) {
func computeTrustScore(_ js.Value, args []js.Value) (any, error) {
// computeTrustScore(metrics: TrustMetrics) -> score
if len(args) < 1 {
return nil, newErr(ErrCodeBadRequest, "computeTrustScore(metrics) requires 'metrics' argument")
return nil, errors.New("computeTrustScore(metrics)")
}
m := args[0]
metrics := pd.TrustMetrics{
@ -557,9 +482,9 @@ func getDefaultPeerFeatureRanges(_ js.Value, _ []js.Value) (any, error) {
func normalizePeerFeatures(_ js.Value, args []js.Value) (any, error) {
// normalizePeerFeatures(features: number[], ranges?: FeatureRanges) -> number[]
if len(args) < 1 {
return nil, newErr(ErrCodeBadRequest, "normalizePeerFeatures(features) requires 'features' argument")
return nil, errors.New("normalizePeerFeatures(features)")
}
features, err := getFloatSlice(args[0], "features")
features, err := getFloatSlice(args[0])
if err != nil {
return nil, err
}
@ -587,13 +512,13 @@ func normalizePeerFeatures(_ js.Value, args []js.Value) (any, error) {
func weightedPeerFeatures(_ js.Value, args []js.Value) (any, error) {
// weightedPeerFeatures(normalized: number[], weights: number[]) -> number[]
if len(args) < 2 {
return nil, newErr(ErrCodeBadRequest, "weightedPeerFeatures(normalized, weights) requires 'normalized' and 'weights' arguments")
return nil, errors.New("weightedPeerFeatures(normalized, weights)")
}
normalized, err := getFloatSlice(args[0], "normalized")
normalized, err := getFloatSlice(args[0])
if err != nil {
return nil, err
}
weights, err := getFloatSlice(args[1], "weights")
weights, err := getFloatSlice(args[1])
if err != nil {
return nil, err
}
@ -609,7 +534,7 @@ func weightedPeerFeatures(_ js.Value, args []js.Value) (any, error) {
func getExternalToolLinks(_ js.Value, args []js.Value) (any, error) {
// getExternalToolLinks(domain: string) -> ExternalToolLinks
if len(args) < 1 {
return nil, newErr(ErrCodeBadRequest, "getExternalToolLinks(domain) requires 'domain' argument")
return nil, errors.New("getExternalToolLinks(domain)")
}
domain := args[0].String()
links := pd.GetExternalToolLinks(domain)
@ -619,7 +544,7 @@ func getExternalToolLinks(_ js.Value, args []js.Value) (any, error) {
func getExternalToolLinksIP(_ js.Value, args []js.Value) (any, error) {
// getExternalToolLinksIP(ip: string) -> ExternalToolLinks
if len(args) < 1 {
return nil, newErr(ErrCodeBadRequest, "getExternalToolLinksIP(ip) requires 'ip' argument")
return nil, errors.New("getExternalToolLinksIP(ip)")
}
ip := args[0].String()
links := pd.GetExternalToolLinksIP(ip)
@ -629,7 +554,7 @@ func getExternalToolLinksIP(_ js.Value, args []js.Value) (any, error) {
func getExternalToolLinksEmail(_ js.Value, args []js.Value) (any, error) {
// getExternalToolLinksEmail(emailOrDomain: string) -> ExternalToolLinks
if len(args) < 1 {
return nil, newErr(ErrCodeBadRequest, "getExternalToolLinksEmail(emailOrDomain) requires 'emailOrDomain' argument")
return nil, errors.New("getExternalToolLinksEmail(emailOrDomain)")
}
emailOrDomain := args[0].String()
links := pd.GetExternalToolLinksEmail(emailOrDomain)
@ -708,7 +633,7 @@ func getRDAPServers(_ js.Value, _ []js.Value) (any, error) {
func buildRDAPDomainURL(_ js.Value, args []js.Value) (any, error) {
// buildRDAPDomainURL(domain: string) -> string
if len(args) < 1 {
return nil, newErr(ErrCodeBadRequest, "buildRDAPDomainURL(domain) requires 'domain' argument")
return nil, errors.New("buildRDAPDomainURL(domain)")
}
domain := args[0].String()
// Use universal RDAP redirector
@ -718,7 +643,7 @@ func buildRDAPDomainURL(_ js.Value, args []js.Value) (any, error) {
func buildRDAPIPURL(_ js.Value, args []js.Value) (any, error) {
// buildRDAPIPURL(ip: string) -> string
if len(args) < 1 {
return nil, newErr(ErrCodeBadRequest, "buildRDAPIPURL(ip) requires 'ip' argument")
return nil, errors.New("buildRDAPIPURL(ip)")
}
ip := args[0].String()
return fmt.Sprintf("https://rdap.org/ip/%s", ip), nil
@ -727,7 +652,7 @@ func buildRDAPIPURL(_ js.Value, args []js.Value) (any, error) {
func buildRDAPASNURL(_ js.Value, args []js.Value) (any, error) {
// buildRDAPASNURL(asn: string) -> string
if len(args) < 1 {
return nil, newErr(ErrCodeBadRequest, "buildRDAPASNURL(asn) requires 'asn' argument")
return nil, errors.New("buildRDAPASNURL(asn)")
}
asn := args[0].String()
// Normalize ASN