clawpowers 1.1.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/COMPATIBILITY.md +13 -0
  3. package/KNOWN_LIMITATIONS.md +19 -0
  4. package/LICENSE +44 -0
  5. package/LICENSING.md +10 -0
  6. package/README.md +378 -210
  7. package/SECURITY.md +52 -0
  8. package/dist/index.d.ts +1477 -0
  9. package/dist/index.js +3464 -0
  10. package/dist/index.js.map +1 -0
  11. package/native/Cargo.lock +4863 -0
  12. package/native/Cargo.toml +73 -0
  13. package/native/crates/canonical/Cargo.toml +24 -0
  14. package/native/crates/canonical/src/lib.rs +673 -0
  15. package/native/crates/compression/Cargo.toml +20 -0
  16. package/native/crates/compression/benches/compression_bench.rs +42 -0
  17. package/native/crates/compression/src/lib.rs +393 -0
  18. package/native/crates/evm-eth/Cargo.toml +13 -0
  19. package/native/crates/evm-eth/src/lib.rs +105 -0
  20. package/native/crates/fee/Cargo.toml +15 -0
  21. package/native/crates/fee/src/lib.rs +281 -0
  22. package/native/crates/index/Cargo.toml +16 -0
  23. package/native/crates/index/src/lib.rs +277 -0
  24. package/native/crates/policy/Cargo.toml +17 -0
  25. package/native/crates/policy/src/lib.rs +614 -0
  26. package/native/crates/security/Cargo.toml +22 -0
  27. package/native/crates/security/src/lib.rs +478 -0
  28. package/native/crates/tokens/Cargo.toml +13 -0
  29. package/native/crates/tokens/src/lib.rs +534 -0
  30. package/native/crates/verification/Cargo.toml +23 -0
  31. package/native/crates/verification/src/lib.rs +333 -0
  32. package/native/crates/wallet/Cargo.toml +20 -0
  33. package/native/crates/wallet/src/lib.rs +261 -0
  34. package/native/crates/x402/Cargo.toml +30 -0
  35. package/native/crates/x402/src/lib.rs +423 -0
  36. package/native/ffi/Cargo.toml +34 -0
  37. package/native/ffi/build.rs +4 -0
  38. package/native/ffi/index.node +0 -0
  39. package/native/ffi/src/lib.rs +352 -0
  40. package/native/ffi/tests/integration.rs +354 -0
  41. package/native/pyo3/Cargo.toml +26 -0
  42. package/native/pyo3/pyproject.toml +16 -0
  43. package/native/pyo3/src/lib.rs +407 -0
  44. package/native/pyo3/tests/test_smoke.py +180 -0
  45. package/native/wasm/Cargo.toml +44 -0
  46. package/native/wasm/pkg/.gitignore +6 -0
  47. package/native/wasm/pkg/clawpowers_wasm.d.ts +208 -0
  48. package/native/wasm/pkg/clawpowers_wasm.js +872 -0
  49. package/native/wasm/pkg/clawpowers_wasm_bg.wasm +0 -0
  50. package/native/wasm/pkg/clawpowers_wasm_bg.wasm.d.ts +40 -0
  51. package/native/wasm/pkg/package.json +17 -0
  52. package/native/wasm/pkg-node/.gitignore +6 -0
  53. package/native/wasm/pkg-node/clawpowers_wasm.d.ts +143 -0
  54. package/native/wasm/pkg-node/clawpowers_wasm.js +798 -0
  55. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm +0 -0
  56. package/native/wasm/pkg-node/clawpowers_wasm_bg.wasm.d.ts +40 -0
  57. package/native/wasm/pkg-node/package.json +13 -0
  58. package/native/wasm/src/lib.rs +433 -0
  59. package/package.json +71 -44
  60. package/src/skills/catalog.ts +435 -0
  61. package/src/skills/executor.ts +56 -0
  62. package/src/skills/index.ts +3 -0
  63. package/src/skills/itp/SKILL.md +112 -0
  64. package/src/skills/loader.ts +193 -0
  65. package/.claude-plugin/manifest.json +0 -19
  66. package/.codex/INSTALL.md +0 -36
  67. package/.cursor-plugin/manifest.json +0 -21
  68. package/.opencode/INSTALL.md +0 -52
  69. package/ARCHITECTURE.md +0 -69
  70. package/bin/clawpowers.js +0 -625
  71. package/bin/clawpowers.sh +0 -91
  72. package/docs/demo/clawpowers-demo.cast +0 -197
  73. package/docs/demo/clawpowers-demo.gif +0 -0
  74. package/docs/launch-images/25-skills-breakdown.jpg +0 -0
  75. package/docs/launch-images/clawpowers-vs-superpowers.jpg +0 -0
  76. package/docs/launch-images/economic-code-optimization.jpg +0 -0
  77. package/docs/launch-images/native-vs-bridge-2.jpg +0 -0
  78. package/docs/launch-images/native-vs-bridge.jpg +0 -0
  79. package/docs/launch-images/post1-hero-lobster.jpg +0 -0
  80. package/docs/launch-images/post2-dashboard.jpg +0 -0
  81. package/docs/launch-images/post3-superpowers.jpg +0 -0
  82. package/docs/launch-images/post4-before-after.jpg +0 -0
  83. package/docs/launch-images/post5-install-now.jpg +0 -0
  84. package/docs/launch-images/ultimate-stack.jpg +0 -0
  85. package/docs/launch-posts.md +0 -76
  86. package/docs/quickstart-first-transaction.md +0 -204
  87. package/gemini-extension.json +0 -32
  88. package/hooks/session-start +0 -205
  89. package/hooks/session-start.cmd +0 -43
  90. package/hooks/session-start.js +0 -163
  91. package/runtime/demo/README.md +0 -78
  92. package/runtime/demo/x402-mock-server.js +0 -230
  93. package/runtime/feedback/analyze.js +0 -621
  94. package/runtime/feedback/analyze.sh +0 -546
  95. package/runtime/init.js +0 -210
  96. package/runtime/init.sh +0 -178
  97. package/runtime/metrics/collector.js +0 -361
  98. package/runtime/metrics/collector.sh +0 -308
  99. package/runtime/payments/ledger.js +0 -305
  100. package/runtime/payments/ledger.sh +0 -262
  101. package/runtime/payments/pipeline.js +0 -455
  102. package/runtime/persistence/store.js +0 -433
  103. package/runtime/persistence/store.sh +0 -303
  104. package/skill.json +0 -106
  105. package/skills/agent-bounties/SKILL.md +0 -553
  106. package/skills/agent-payments/SKILL.md +0 -479
  107. package/skills/brainstorming/SKILL.md +0 -233
  108. package/skills/content-pipeline/SKILL.md +0 -282
  109. package/skills/cross-project-knowledge/SKILL.md +0 -345
  110. package/skills/dispatching-parallel-agents/SKILL.md +0 -305
  111. package/skills/economic-code-optimization/SKILL.md +0 -265
  112. package/skills/executing-plans/SKILL.md +0 -255
  113. package/skills/finishing-a-development-branch/SKILL.md +0 -260
  114. package/skills/formal-verification-lite/SKILL.md +0 -441
  115. package/skills/learn-how-to-learn/SKILL.md +0 -235
  116. package/skills/market-intelligence/SKILL.md +0 -323
  117. package/skills/meta-skill-evolution/SKILL.md +0 -325
  118. package/skills/prospecting/SKILL.md +0 -454
  119. package/skills/receiving-code-review/SKILL.md +0 -225
  120. package/skills/requesting-code-review/SKILL.md +0 -206
  121. package/skills/security-audit/SKILL.md +0 -353
  122. package/skills/self-healing-code/SKILL.md +0 -369
  123. package/skills/subagent-driven-development/SKILL.md +0 -244
  124. package/skills/systematic-debugging/SKILL.md +0 -355
  125. package/skills/test-driven-development/SKILL.md +0 -416
  126. package/skills/using-clawpowers/SKILL.md +0 -160
  127. package/skills/using-git-worktrees/SKILL.md +0 -261
  128. package/skills/validator/SKILL.md +0 -281
  129. package/skills/verification-before-completion/SKILL.md +0 -254
  130. package/skills/writing-plans/SKILL.md +0 -276
  131. package/skills/writing-skills/SKILL.md +0 -260
@@ -0,0 +1,281 @@
1
+ //! clawpowers-fee — Fee schedule calculation for transactions and swaps.
2
+ //!
3
+ //! [`FeeSchedule`] holds basis-point rates for different operation types and
4
+ //! computes the fee for a given [`TokenAmount`].
5
+
6
+ use alloy_primitives::Address;
7
+ use clawpowers_tokens::{TokenAmount, TokenError};
8
+ use serde::{Deserialize, Serialize};
9
+ use thiserror::Error;
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Constants
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /// Default transaction fee: 77 bps = 0.77%.
16
+ pub const DEFAULT_TX_FEE_BPS: u64 = 77;
17
+
18
+ /// Default swap fee: 30 bps = 0.30%.
19
+ pub const DEFAULT_SWAP_FEE_BPS: u64 = 30;
20
+
21
+ /// Placeholder fee recipient address used by [`FeeSchedule::default`].
22
+ pub const PLACEHOLDER_FEE_RECIPIENT: Address = Address::ZERO;
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // FeeType
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /// Fee classification.
29
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30
+ #[serde(rename_all = "snake_case")]
31
+ pub enum FeeType {
32
+ /// Standard on-chain transaction fee.
33
+ Transaction,
34
+ /// DEX swap fee.
35
+ Swap,
36
+ /// Custom fee with explicit basis points.
37
+ Custom(u64),
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Errors
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /// Errors produced by fee operations.
45
+ #[derive(Debug, Error, PartialEq, Eq)]
46
+ pub enum FeeError {
47
+ /// Token arithmetic failed.
48
+ #[error("token arithmetic error: {0}")]
49
+ Token(String),
50
+ /// Basis-point rate exceeds 10 000 (100%).
51
+ #[error("invalid basis points: {0} (max 10000)")]
52
+ InvalidBps(u64),
53
+ /// Computed fee would exceed the gross amount.
54
+ #[error("fee ({fee_bps} bps) would exceed gross amount")]
55
+ FeeExceedsAmount {
56
+ /// The basis-point rate that caused the violation.
57
+ fee_bps: u64,
58
+ },
59
+ }
60
+
61
+ impl From<TokenError> for FeeError {
62
+ fn from(e: TokenError) -> Self {
63
+ FeeError::Token(e.to_string())
64
+ }
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // FeeCalculation
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /// The result of a fee calculation.
72
+ #[derive(Debug, Clone)]
73
+ pub struct FeeCalculation {
74
+ /// The original, pre-fee amount.
75
+ pub gross_amount: TokenAmount,
76
+ /// The fee portion deducted from `gross_amount`.
77
+ pub fee_amount: TokenAmount,
78
+ /// The amount remaining after fee deduction.
79
+ pub net_amount: TokenAmount,
80
+ /// The address that will receive the fee.
81
+ pub fee_recipient: Address,
82
+ /// The type of fee that was applied.
83
+ pub fee_type: FeeType,
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // FeeSchedule
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /// Configures fee rates and the recipient address for the protocol.
91
+ #[derive(Debug, Clone)]
92
+ pub struct FeeSchedule {
93
+ /// Transaction fee in basis points (default: 77 = 0.77%).
94
+ pub tx_fee_bps: u64,
95
+ /// Swap fee in basis points (default: 30 = 0.30%).
96
+ pub swap_fee_bps: u64,
97
+ /// Address that accumulates collected fees.
98
+ pub fee_recipient: Address,
99
+ }
100
+
101
+ impl Default for FeeSchedule {
102
+ fn default() -> Self {
103
+ Self {
104
+ tx_fee_bps: DEFAULT_TX_FEE_BPS,
105
+ swap_fee_bps: DEFAULT_SWAP_FEE_BPS,
106
+ fee_recipient: PLACEHOLDER_FEE_RECIPIENT,
107
+ }
108
+ }
109
+ }
110
+
111
+ impl FeeSchedule {
112
+ /// Creates a new [`FeeSchedule`] with explicit parameters.
113
+ pub fn new(tx_fee_bps: u64, swap_fee_bps: u64, fee_recipient: Address) -> Self {
114
+ Self {
115
+ tx_fee_bps,
116
+ swap_fee_bps,
117
+ fee_recipient,
118
+ }
119
+ }
120
+
121
+ /// Returns the effective basis-point rate for the given `fee_type`.
122
+ fn bps_for(&self, fee_type: &FeeType) -> u64 {
123
+ match fee_type {
124
+ FeeType::Transaction => self.tx_fee_bps,
125
+ FeeType::Swap => self.swap_fee_bps,
126
+ FeeType::Custom(bps) => *bps,
127
+ }
128
+ }
129
+
130
+ /// Calculates a fee for `amount` and the given `fee_type`.
131
+ ///
132
+ /// # Errors
133
+ ///
134
+ /// - [`FeeError::InvalidBps`] — rate exceeds 10 000.
135
+ /// - [`FeeError::Token`] — arithmetic overflow.
136
+ /// - [`FeeError::FeeExceedsAmount`] — fee ≥ gross.
137
+ pub fn calculate(
138
+ &self,
139
+ amount: TokenAmount,
140
+ fee_type: FeeType,
141
+ ) -> Result<FeeCalculation, FeeError> {
142
+ let bps = self.bps_for(&fee_type);
143
+ if bps > 10_000 {
144
+ return Err(FeeError::InvalidBps(bps));
145
+ }
146
+
147
+ let fee_amount = amount
148
+ .checked_mul_bps(bps)
149
+ .ok_or_else(|| FeeError::Token("overflow in checked_mul_bps".to_string()))?;
150
+
151
+ let net_amount = amount
152
+ .sub(&fee_amount)
153
+ .map_err(|_| FeeError::FeeExceedsAmount { fee_bps: bps })?;
154
+
155
+ Ok(FeeCalculation {
156
+ gross_amount: amount,
157
+ fee_amount,
158
+ net_amount,
159
+ fee_recipient: self.fee_recipient,
160
+ fee_type,
161
+ })
162
+ }
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Tests
167
+ // ---------------------------------------------------------------------------
168
+
169
+ #[cfg(test)]
170
+ mod tests {
171
+ use super::*;
172
+ use alloy_primitives::Address;
173
+
174
+ fn usdc(amount: f64) -> TokenAmount {
175
+ TokenAmount::from_human(amount, 6)
176
+ }
177
+
178
+ fn eth(amount: f64) -> TokenAmount {
179
+ TokenAmount::from_human(amount, 18)
180
+ }
181
+
182
+ #[test]
183
+ fn default_fee_schedule_values() {
184
+ let s = FeeSchedule::default();
185
+ assert_eq!(s.tx_fee_bps, 77);
186
+ assert_eq!(s.swap_fee_bps, 30);
187
+ assert_eq!(s.fee_recipient, Address::ZERO);
188
+ }
189
+
190
+ #[test]
191
+ fn tx_fee_usdc_1000() {
192
+ let s = FeeSchedule::default();
193
+ let calc = s.calculate(usdc(1000.0), FeeType::Transaction).unwrap();
194
+ // 1000 USDC × 77/10000 = 7.7 USDC
195
+ assert!((calc.fee_amount.to_human() - 7.7).abs() < 0.000_001);
196
+ assert!((calc.net_amount.to_human() - 992.3).abs() < 0.000_001);
197
+ assert_eq!(calc.fee_type, FeeType::Transaction);
198
+ }
199
+
200
+ #[test]
201
+ fn tx_fee_usdc_1() {
202
+ let s = FeeSchedule::default();
203
+ let calc = s.calculate(usdc(1.0), FeeType::Transaction).unwrap();
204
+ // 1 USDC × 77/10000 = 0.0077 USDC
205
+ assert!((calc.fee_amount.to_human() - 0.0077).abs() < 0.000_001);
206
+ }
207
+
208
+ #[test]
209
+ fn swap_fee_usdc_1000() {
210
+ let s = FeeSchedule::default();
211
+ let calc = s.calculate(usdc(1000.0), FeeType::Swap).unwrap();
212
+ // 1000 USDC × 30/10000 = 3 USDC
213
+ assert!((calc.fee_amount.to_human() - 3.0).abs() < 0.000_001);
214
+ assert_eq!(calc.fee_type, FeeType::Swap);
215
+ }
216
+
217
+ #[test]
218
+ fn swap_fee_custom_50bps() {
219
+ let s = FeeSchedule::new(30, 50, Address::ZERO);
220
+ let calc = s.calculate(usdc(200.0), FeeType::Swap).unwrap();
221
+ // 200 USDC × 50/10000 = 1 USDC
222
+ assert!((calc.fee_amount.to_human() - 1.0).abs() < 0.000_001);
223
+ }
224
+
225
+ #[test]
226
+ fn tx_fee_eth_1() {
227
+ let s = FeeSchedule::default();
228
+ let calc = s.calculate(eth(1.0), FeeType::Transaction).unwrap();
229
+ // 1 ETH × 77/10000 = 0.0077 ETH
230
+ assert!((calc.fee_amount.to_human() - 0.0077).abs() < 0.000_001);
231
+ }
232
+
233
+ #[test]
234
+ fn swap_fee_eth_10() {
235
+ let s = FeeSchedule::default();
236
+ let calc = s.calculate(eth(10.0), FeeType::Swap).unwrap();
237
+ // 10 ETH × 30/10000 = 0.03 ETH
238
+ assert!((calc.fee_amount.to_human() - 0.03).abs() < 0.000_001);
239
+ }
240
+
241
+ #[test]
242
+ fn zero_amount_returns_zero_fee() {
243
+ let s = FeeSchedule::default();
244
+ let calc = s.calculate(usdc(0.0), FeeType::Transaction).unwrap();
245
+ assert!(calc.fee_amount.is_zero());
246
+ assert!(calc.net_amount.is_zero());
247
+ }
248
+
249
+ #[test]
250
+ fn custom_fee_type_100bps() {
251
+ let s = FeeSchedule::default();
252
+ let calc = s.calculate(usdc(100.0), FeeType::Custom(100)).unwrap();
253
+ // 100 USDC × 100/10000 = 1 USDC
254
+ assert!((calc.fee_amount.to_human() - 1.0).abs() < 0.000_001);
255
+ }
256
+
257
+ #[test]
258
+ fn fee_recipient_propagated() {
259
+ use alloy_primitives::address;
260
+ let recipient = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
261
+ let s = FeeSchedule::new(77, 30, recipient);
262
+ let calc = s.calculate(usdc(500.0), FeeType::Transaction).unwrap();
263
+ assert_eq!(calc.fee_recipient, recipient);
264
+ }
265
+
266
+ #[test]
267
+ fn gross_equals_fee_plus_net() {
268
+ let s = FeeSchedule::default();
269
+ let gross = usdc(123.456789);
270
+ let calc = s.calculate(gross.clone(), FeeType::Swap).unwrap();
271
+ let reconstructed = calc.fee_amount.add(&calc.net_amount).unwrap();
272
+ assert_eq!(reconstructed, calc.gross_amount);
273
+ }
274
+
275
+ #[test]
276
+ fn bps_over_10000_is_rejected() {
277
+ let s = FeeSchedule::new(10_001, 30, Address::ZERO);
278
+ let err = s.calculate(usdc(1.0), FeeType::Transaction).unwrap_err();
279
+ assert_eq!(err, FeeError::InvalidBps(10_001));
280
+ }
281
+ }
@@ -0,0 +1,16 @@
1
+ [package]
2
+ name = "clawpowers-index"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ license.workspace = true
6
+
7
+ [dependencies]
8
+ serde = { workspace = true }
9
+ serde_json = { workspace = true }
10
+ thiserror = { workspace = true }
11
+ tracing = { workspace = true }
12
+ uuid = { workspace = true }
13
+ clawpowers-compression = { path = "../compression" }
14
+
15
+ [dev-dependencies]
16
+ rand = { workspace = true }
@@ -0,0 +1,277 @@
1
+ //! Vector index adapter for TurboMemory.
2
+ //!
3
+ //! Exposes a [`VectorIndex`] trait and an [`InMemoryIndex`] implementation
4
+ //! that stores compressed vectors via [`TurboCompressor`] and retrieves
5
+ //! nearest neighbours using brute-force cosine similarity.
6
+
7
+ use clawpowers_compression::{CompressedVector, CompressionConfig, TurboCompressor};
8
+ use thiserror::Error;
9
+ use uuid::Uuid;
10
+
11
+ /// Errors produced by the index.
12
+ #[derive(Debug, Error)]
13
+ pub enum IndexError {
14
+ /// The query vector has a different dimensionality than the index.
15
+ #[error("dimension mismatch: expected {expected}, got {got}")]
16
+ DimensionMismatch {
17
+ /// Expected dimensionality.
18
+ expected: usize,
19
+ /// Provided dimensionality.
20
+ got: usize,
21
+ },
22
+ /// The requested number of results is zero.
23
+ #[error("top_k must be > 0")]
24
+ ZeroTopK,
25
+ /// Underlying compression error.
26
+ #[error("compression error: {0}")]
27
+ Compression(#[from] clawpowers_compression::CompressionError),
28
+ }
29
+
30
+ /// A shorthand result type for [`IndexError`].
31
+ pub type Result<T> = std::result::Result<T, IndexError>;
32
+
33
+ /// A single ranked search result.
34
+ #[derive(Debug, Clone)]
35
+ pub struct SearchResult {
36
+ /// Identifier of the matching vector.
37
+ pub id: Uuid,
38
+ /// Similarity score (cosine similarity, higher is more similar).
39
+ pub score: f32,
40
+ }
41
+
42
+ /// Trait for vector stores used by TurboMemory.
43
+ pub trait VectorIndex {
44
+ /// Insert a vector under `id`.
45
+ fn insert(&mut self, id: Uuid, vector: Vec<f32>) -> Result<()>;
46
+ /// Return the `top_k` most similar vectors to `query`, ranked descending by score.
47
+ fn search(&self, query: &[f32], top_k: usize) -> Result<Vec<SearchResult>>;
48
+ /// Remove the vector with `id`. Returns `true` if it was present.
49
+ fn remove(&mut self, id: &Uuid) -> Result<bool>;
50
+ /// Number of vectors currently in the index.
51
+ fn len(&self) -> usize;
52
+ /// Return `true` if the index contains no vectors.
53
+ fn is_empty(&self) -> bool {
54
+ self.len() == 0
55
+ }
56
+ }
57
+
58
+ struct Entry {
59
+ id: Uuid,
60
+ /// Stored for potential future fast-path approximate distance calculations.
61
+ #[allow(dead_code)]
62
+ compressed: CompressedVector,
63
+ /// Original vector used for exact cosine similarity.
64
+ original: Vec<f32>,
65
+ }
66
+
67
+ /// In-memory brute-force vector index backed by TurboQuant compression.
68
+ ///
69
+ /// Suitable for up to ~100 K vectors.
70
+ pub struct InMemoryIndex {
71
+ compressor: TurboCompressor,
72
+ entries: Vec<Entry>,
73
+ }
74
+
75
+ impl InMemoryIndex {
76
+ /// Create a new index with the given compression configuration.
77
+ pub fn new(config: CompressionConfig) -> Self {
78
+ let compressor = TurboCompressor::new(config);
79
+ Self {
80
+ compressor,
81
+ entries: Vec::new(),
82
+ }
83
+ }
84
+
85
+ /// Create an index with sensible defaults for `dimensions`.
86
+ pub fn with_dimensions(dimensions: usize) -> Self {
87
+ Self::new(CompressionConfig {
88
+ dimensions,
89
+ quantization_bits: 8,
90
+ rotation_seed: 0xDEAD_BEEF_CAFE_1234,
91
+ })
92
+ }
93
+ }
94
+
95
+ impl VectorIndex for InMemoryIndex {
96
+ fn insert(&mut self, id: Uuid, vector: Vec<f32>) -> Result<()> {
97
+ let compressed = self.compressor.compress(&vector)?;
98
+ self.entries.push(Entry {
99
+ id,
100
+ compressed,
101
+ original: vector,
102
+ });
103
+ Ok(())
104
+ }
105
+
106
+ fn search(&self, query: &[f32], top_k: usize) -> Result<Vec<SearchResult>> {
107
+ if top_k == 0 {
108
+ return Err(IndexError::ZeroTopK);
109
+ }
110
+ let dim = self.compressor.config.dimensions;
111
+ if query.len() != dim {
112
+ return Err(IndexError::DimensionMismatch {
113
+ expected: dim,
114
+ got: query.len(),
115
+ });
116
+ }
117
+ let mut scored: Vec<(f32, &Entry)> = self
118
+ .entries
119
+ .iter()
120
+ .map(|e| (cosine_similarity(query, &e.original), e))
121
+ .collect();
122
+ scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
123
+ Ok(scored
124
+ .into_iter()
125
+ .take(top_k)
126
+ .map(|(score, e)| SearchResult { id: e.id, score })
127
+ .collect())
128
+ }
129
+
130
+ fn remove(&mut self, id: &Uuid) -> Result<bool> {
131
+ let before = self.entries.len();
132
+ self.entries.retain(|e| &e.id != id);
133
+ Ok(self.entries.len() < before)
134
+ }
135
+
136
+ fn len(&self) -> usize {
137
+ self.entries.len()
138
+ }
139
+ }
140
+
141
+ fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
142
+ let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
143
+ let na: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
144
+ let nb: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
145
+ if na < f32::EPSILON || nb < f32::EPSILON {
146
+ 0.0
147
+ } else {
148
+ dot / (na * nb)
149
+ }
150
+ }
151
+
152
+ #[cfg(test)]
153
+ mod tests {
154
+ use super::*;
155
+
156
+ const DIM: usize = 32;
157
+
158
+ fn make_index() -> InMemoryIndex {
159
+ InMemoryIndex::with_dimensions(DIM)
160
+ }
161
+
162
+ fn unit_vec(hot: usize) -> Vec<f32> {
163
+ let mut v = vec![0.0_f32; DIM];
164
+ v[hot % DIM] = 1.0;
165
+ v
166
+ }
167
+
168
+ fn rand_vec(seed: u64) -> Vec<f32> {
169
+ let mut x = seed;
170
+ (0..DIM)
171
+ .map(|_| {
172
+ x = x
173
+ .wrapping_mul(6_364_136_223_846_793_005)
174
+ .wrapping_add(1_442_695_040_888_963_407);
175
+ ((x >> 33) as f32 / u32::MAX as f32) * 2.0 - 1.0
176
+ })
177
+ .collect()
178
+ }
179
+
180
+ #[test]
181
+ fn test_insert_increases_len() {
182
+ let mut idx = make_index();
183
+ assert_eq!(idx.len(), 0);
184
+ idx.insert(Uuid::new_v4(), rand_vec(1)).expect("insert");
185
+ assert_eq!(idx.len(), 1);
186
+ }
187
+
188
+ #[test]
189
+ fn test_empty_index_search_returns_empty() {
190
+ let idx = make_index();
191
+ assert!(idx.search(&rand_vec(2), 5).expect("search").is_empty());
192
+ }
193
+
194
+ #[test]
195
+ fn test_is_empty() {
196
+ let mut idx = make_index();
197
+ assert!(idx.is_empty());
198
+ idx.insert(Uuid::new_v4(), rand_vec(3)).expect("insert");
199
+ assert!(!idx.is_empty());
200
+ }
201
+
202
+ #[test]
203
+ fn test_search_returns_nearest_neighbour() {
204
+ let mut idx = make_index();
205
+ let query = unit_vec(0);
206
+ let id_match = Uuid::new_v4();
207
+ let id_other = Uuid::new_v4();
208
+ idx.insert(id_match, unit_vec(0)).expect("insert match");
209
+ idx.insert(id_other, unit_vec(1)).expect("insert other");
210
+ let results = idx.search(&query, 1).expect("search");
211
+ assert_eq!(results.len(), 1);
212
+ assert_eq!(results[0].id, id_match);
213
+ }
214
+
215
+ #[test]
216
+ fn test_search_results_ordered_by_score_descending() {
217
+ let mut idx = make_index();
218
+ let query = rand_vec(42);
219
+ for i in 0..5 {
220
+ idx.insert(Uuid::new_v4(), rand_vec(i + 100))
221
+ .expect("insert");
222
+ }
223
+ let results = idx.search(&query, 5).expect("search");
224
+ for w in results.windows(2) {
225
+ assert!(w[0].score >= w[1].score, "{} < {}", w[0].score, w[1].score);
226
+ }
227
+ }
228
+
229
+ #[test]
230
+ fn test_search_top_k_limits_results() {
231
+ let mut idx = make_index();
232
+ for i in 0..10 {
233
+ idx.insert(Uuid::new_v4(), rand_vec(i)).expect("insert");
234
+ }
235
+ assert_eq!(idx.search(&rand_vec(99), 3).expect("search").len(), 3);
236
+ }
237
+
238
+ #[test]
239
+ fn test_remove_existing_returns_true() {
240
+ let mut idx = make_index();
241
+ let id = Uuid::new_v4();
242
+ idx.insert(id, rand_vec(5)).expect("insert");
243
+ assert!(idx.remove(&id).expect("remove"));
244
+ assert_eq!(idx.len(), 0);
245
+ }
246
+
247
+ #[test]
248
+ fn test_remove_nonexistent_returns_false() {
249
+ let mut idx = make_index();
250
+ assert!(!idx.remove(&Uuid::new_v4()).expect("remove"));
251
+ }
252
+
253
+ #[test]
254
+ fn test_removed_vector_not_in_results() {
255
+ let mut idx = make_index();
256
+ let query = unit_vec(0);
257
+ let id = Uuid::new_v4();
258
+ idx.insert(id, unit_vec(0)).expect("insert");
259
+ idx.remove(&id).expect("remove");
260
+ assert!(
261
+ !idx.search(&query, 10)
262
+ .expect("search")
263
+ .iter()
264
+ .any(|r| r.id == id)
265
+ );
266
+ }
267
+
268
+ #[test]
269
+ fn test_zero_top_k_errors() {
270
+ let mut idx = make_index();
271
+ let _ = idx.insert(Uuid::new_v4(), rand_vec(6));
272
+ assert!(matches!(
273
+ idx.search(&rand_vec(7), 0).expect_err("e"),
274
+ IndexError::ZeroTopK
275
+ ));
276
+ }
277
+ }
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "clawpowers-policy"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ license.workspace = true
6
+
7
+ [dependencies]
8
+ serde = { workspace = true }
9
+ serde_json = { workspace = true }
10
+ thiserror = { workspace = true }
11
+ tracing = { workspace = true }
12
+ chrono = { workspace = true }
13
+ alloy-primitives = { workspace = true }
14
+ clawpowers-tokens = { path = "../tokens" }
15
+
16
+ [dev-dependencies]
17
+ tokio = { workspace = true }