Merge feature/janus-sdk-v0.1.0 into unstable
Sprint 2 Complete: - GQL Parser (ISO/IEC 39075:2024 compliant) - GQL to Zig Code Generator - Comprehensive SDK Documentation - Bellman-Ford Betrayal Detection - 166/166 tests passing (green build)
This commit is contained in:
commit
abea07febf
|
|
@ -38,3 +38,9 @@ capsule.log
|
|||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# BDD Specifications - Gold worth, protect from competitors
|
||||
# Uncomment below to share specs with trusted collaborators only
|
||||
*.feature
|
||||
features/
|
||||
!features/README.md
|
||||
|
|
|
|||
15
build.zig
15
build.zig
|
|
@ -200,6 +200,18 @@ pub fn build(b: *std.Build) void {
|
|||
// trust_graph needs crypto types
|
||||
l1_trust_graph_mod.addImport("crypto", l1_mod);
|
||||
|
||||
// ========================================================================
|
||||
// L1 Proof of Path Module (PoP)
|
||||
// ========================================================================
|
||||
const l1_pop_mod = b.createModule(.{
|
||||
.root_source_file = b.path("l1-identity/proof_of_path.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
l1_pop_mod.addImport("trust_graph", l1_trust_graph_mod);
|
||||
l1_pop_mod.addImport("time", time_mod);
|
||||
l1_pop_mod.addImport("soulkey", l1_soulkey_mod);
|
||||
|
||||
// ========================================================================
|
||||
// L1 QVL (Quasar Vector Lattice) - Advanced Graph Engine
|
||||
// ========================================================================
|
||||
|
|
@ -209,7 +221,10 @@ pub fn build(b: *std.Build) void {
|
|||
.optimize = optimize,
|
||||
});
|
||||
l1_qvl_mod.addImport("trust_graph", l1_trust_graph_mod);
|
||||
l1_qvl_mod.addImport("proof_of_path", l1_pop_mod);
|
||||
l1_qvl_mod.addImport("time", time_mod);
|
||||
// Note: libmdbx linking removed - using stub implementation for now
|
||||
// TODO: Add real libmdbx when available on build system
|
||||
|
||||
// QVL FFI (C ABI exports for L2 integration)
|
||||
const l1_qvl_ffi_mod = b.createModule(.{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
# QVL BDD Test Suite
|
||||
|
||||
## Overview
|
||||
This directory contains Gherkin feature specifications for the Quasar Vector Lattice (QVL) - L1 trust graph engine.
|
||||
|
||||
**Status:** Sprint 0 — Specification Complete
|
||||
**Next:** Implement step definitions in Zig
|
||||
|
||||
---
|
||||
|
||||
## Feature Files
|
||||
|
||||
| Feature | Scenarios | Purpose |
|
||||
|---------|-----------|---------|
|
||||
| `trust_graph.feature` | 8 | Core graph operations (add/remove/query edges) |
|
||||
| `betrayal_detection.feature` | 8 | Bellman-Ford negative cycle detection |
|
||||
| `pathfinding.feature` | 10 | A* reputation-guided pathfinding |
|
||||
| `gossip_protocol.feature` | 10 | Aleph-style probabilistic flooding |
|
||||
| `belief_propagation.feature` | 8 | Bayesian inference over trust DAG |
|
||||
| `pop_reputation.feature` | 14 | PoP verification + reputation scoring |
|
||||
|
||||
**Total:** 58 scenarios covering all QVL functionality
|
||||
|
||||
---
|
||||
|
||||
## Key Testing Principles
|
||||
|
||||
### Kenya Rule Compliance
|
||||
Every feature includes performance scenarios:
|
||||
- Memory usage < 10MB
|
||||
- Execution time benchmarks for O(|V|×|E|) algorithms
|
||||
- Bandwidth limits for gossip
|
||||
|
||||
### Security Coverage
|
||||
- Betrayal detection (negative cycles)
|
||||
- Eclipse attack resilience
|
||||
- Replay protection (entropy stamps)
|
||||
- Signature verification
|
||||
|
||||
### Integration Points
|
||||
- PoP (Proof-of-Path) verification
|
||||
- Reputation decay over time
|
||||
- RiskGraph → CompactTrustGraph mapping
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Future: Zig Implementation
|
||||
```bash
|
||||
# Run all QVL tests
|
||||
zig build test-qvl
|
||||
|
||||
# Run specific feature
|
||||
zig build test -- --feature betrayal_detection
|
||||
|
||||
# Run with coverage
|
||||
zig build test-qvl-coverage
|
||||
```
|
||||
|
||||
### Current: Documentation Phase
|
||||
These features serve as:
|
||||
1. **Specification** — What QVL should do
|
||||
2. **Acceptance Criteria** — When we're done
|
||||
3. **Documentation** — How it works
|
||||
4. **Test Template** — For Zig implementation
|
||||
|
||||
---
|
||||
|
||||
## GQL Integration (Future)
|
||||
|
||||
When GQL Parser is implemented:
|
||||
```gherkin
|
||||
Scenario: GQL query for trust path
|
||||
When I execute GQL "MATCH (a:Identity)-[t:TRUST*1..3]->(b:Identity) WHERE a.did = 'did:alice' RETURN b"
|
||||
Then I should receive reachable nodes within 3 hops
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- `../l1-identity/qvl/` — Implementation (Zig)
|
||||
- `../../docs/L4-hybrid-schema.md` — L4 Feed schema
|
||||
- RFC-0120 — QVL Specification
|
||||
|
||||
---
|
||||
|
||||
**Maintainer:** Frankie (Silicon Architect)
|
||||
**Last Updated:** 2026-02-03
|
||||
|
||||
⚡️
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
Feature: Loopy Belief Propagation
|
||||
As a Libertaria node under eclipse attack
|
||||
I need Bayesian inference over the trust DAG
|
||||
So that I can estimate trust under uncertainty and detect anomalies
|
||||
|
||||
Background:
|
||||
Given a trust graph with partial visibility:
|
||||
| from | to | observed | prior_trust |
|
||||
| alice | bob | true | 0.6 |
|
||||
| bob | charlie | false | unknown |
|
||||
| alice | dave | true | 0.8 |
|
||||
|
||||
# Belief Propagation Core
|
||||
Scenario: Propagate beliefs through observed edges
|
||||
When I run Belief Propagation from "alice"
|
||||
Then the belief for "bob" should converge to ~0.6
|
||||
And the belief for "alice" should be 1.0 (self-trust)
|
||||
|
||||
Scenario: Infer unobserved edge from network structure
|
||||
Given "alice" trusts "bob" (0.6)
|
||||
And "bob" is likely to trust "charlie" (transitivity)
|
||||
When I run BP with max_iterations 100
|
||||
Then the belief for "charlie" should be > 0.5
|
||||
And < 0.6 (less certain than direct observation)
|
||||
|
||||
Scenario: Convergence detection
|
||||
When I run BP with epsilon 1e-6
|
||||
Then the algorithm should stop when max belief delta < epsilon
|
||||
And the converged flag should be true
|
||||
And iterations should be < max_iterations
|
||||
|
||||
Scenario: Non-convergence handling
|
||||
Given a graph with oscillating beliefs (bipartite structure)
|
||||
When I run BP with damping 0.5
|
||||
Then the algorithm should force convergence via damping
|
||||
Or report non-convergence after max_iterations
|
||||
|
||||
# Anomaly Scoring
|
||||
Scenario: Anomaly from BP divergence
|
||||
Given a node with belief 0.9 from one path
|
||||
And belief 0.1 from another path (conflict)
|
||||
When BP converges
|
||||
Then the anomaly score should be high (> 0.7)
|
||||
And the reason should be "bp_divergence"
|
||||
|
||||
Scenario: Eclipse attack detection
|
||||
Given an adversary controls 90% of observed edges to "victim"
|
||||
And the adversary reports uniformly positive trust
|
||||
When BP runs with honest nodes as priors
|
||||
Then the victim's belief should remain moderate (not extreme)
|
||||
And the coverage metric should indicate "potential_eclipse"
|
||||
|
||||
# Damping and Stability
|
||||
Scenario Outline: Damping factor effects
|
||||
Given a graph prone to oscillation
|
||||
When I run BP with damping <damping>
|
||||
Then convergence should occur in <iterations> iterations
|
||||
|
||||
Examples:
|
||||
| damping | iterations |
|
||||
| 0.0 | > 100 |
|
||||
| 0.5 | ~50 |
|
||||
| 0.9 | ~20 |
|
||||
|
||||
# Integration with Bellman-Ford
|
||||
Scenario: BP complements negative cycle detection
|
||||
Given a graph with a near-negative-cycle (ambiguous betrayal)
|
||||
When Bellman-Ford is inconclusive
|
||||
And BP reports high anomaly for involved nodes
|
||||
Then the combined evidence suggests investigation
|
||||
|
||||
# Performance Constraints
|
||||
Scenario: BP complexity
|
||||
Given a graph with 1000 nodes and 5000 edges
|
||||
When I run BP with epsilon 1e-6
|
||||
Then convergence should occur within 50 iterations
|
||||
And total time should be < 100ms
|
||||
And memory should be O(|V| + |E|)
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
Feature: Bellman-Ford Betrayal Detection
|
||||
As a Libertaria security node
|
||||
I need to detect negative cycles in the trust graph
|
||||
So that I can identify collusion rings and betrayal patterns
|
||||
|
||||
Background:
|
||||
Given a QVL database with the following trust edges:
|
||||
| from | to | level | risk |
|
||||
| alice | bob | 3 | -0.3 |
|
||||
| bob | charlie | 3 | -0.3 |
|
||||
| charlie | alice | -7 | 1.0 |
|
||||
|
||||
# Negative Cycle Detection
|
||||
Scenario: Detect simple negative cycle (betrayal ring)
|
||||
When I run Bellman-Ford from "alice"
|
||||
Then a negative cycle should be detected
|
||||
And the cycle should contain nodes: "alice", "bob", "charlie"
|
||||
And the anomaly score should be 1.0 (critical)
|
||||
|
||||
Scenario: No cycle in legitimate trust chain
|
||||
Given a QVL database with the following trust edges:
|
||||
| from | to | level | risk |
|
||||
| alice | bob | 3 | -0.3 |
|
||||
| bob | charlie | 3 | -0.3 |
|
||||
| charlie | dave | 3 | -0.3 |
|
||||
When I run Bellman-Ford from "alice"
|
||||
Then no negative cycle should be detected
|
||||
And the anomaly score should be 0.0
|
||||
|
||||
Scenario: Multiple betrayal cycles
|
||||
Given a QVL database with the following trust edges:
|
||||
| from | to | level | risk |
|
||||
| alice | bob | -5 | 0.5 |
|
||||
| bob | alice | -5 | 0.5 |
|
||||
| charlie | dave | -5 | 0.5 |
|
||||
| dave | charlie | -5 | 0.5 |
|
||||
When I run Bellman-Ford from "alice"
|
||||
Then 2 negative cycles should be detected
|
||||
And cycle 1 should contain: "alice", "bob"
|
||||
And cycle 2 should contain: "charlie", "dave"
|
||||
|
||||
# Evidence Generation
|
||||
Scenario: Generate cryptographic evidence of betrayal
|
||||
Given a negative cycle has been detected:
|
||||
| node | risk |
|
||||
| alice | -0.3 |
|
||||
| bob | -0.3 |
|
||||
| charlie | 1.0 |
|
||||
When I generate evidence for the cycle
|
||||
Then the evidence should be a byte array
|
||||
And the evidence version should be 0x01
|
||||
And the evidence should contain all 3 node IDs
|
||||
And the evidence should contain all risk scores
|
||||
And the evidence hash should be deterministic
|
||||
|
||||
Scenario: Evidence serialization format
|
||||
When I generate evidence for a cycle with nodes "alice", "bob"
|
||||
Then the evidence format should be:
|
||||
"""
|
||||
version(1 byte) + cycle_len(4 bytes) +
|
||||
[node_id(4 bytes) + risk(8 bytes)]...
|
||||
"""
|
||||
|
||||
# Performance Constraints (Kenya Rule)
|
||||
Scenario Outline: Bellman-Ford complexity with graph size
|
||||
Given a graph with <nodes> nodes and <edges> edges
|
||||
When I run Bellman-Ford
|
||||
Then the execution time should be less than <time_ms> milliseconds
|
||||
And the memory usage should be less than 10MB
|
||||
|
||||
Examples:
|
||||
| nodes | edges | time_ms |
|
||||
| 100 | 500 | 50 |
|
||||
| 1000 | 5000 | 500 |
|
||||
| 10000 | 50000 | 5000 |
|
||||
|
||||
# Early Exit Optimization
|
||||
Scenario: Early exit when no improvements possible
|
||||
Given a graph where no edges can be relaxed after pass 3
|
||||
When I run Bellman-Ford
|
||||
Then the algorithm should exit after pass 3
|
||||
And not run all |V|-1 passes
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
Feature: Aleph-Style Gossip Protocol
|
||||
As a Libertaria node in a partitioned network
|
||||
I need probabilistic message flooding with DAG references
|
||||
So that trust signals propagate despite intermittent connectivity
|
||||
|
||||
Background:
|
||||
Given a network of 5 nodes: alpha, beta, gamma, delta, epsilon
|
||||
And each node has initialized gossip state
|
||||
And the erasure tolerance parameter k = 3
|
||||
|
||||
# Gossip Message Structure
|
||||
Scenario: Create gossip message with DAG references
|
||||
Given node "alpha" has received messages with IDs [100, 101, 102]
|
||||
When "alpha" creates a gossip message of type "trust_vouch"
|
||||
Then the message should reference k=3 prior messages
|
||||
And the message ID should be computed from (sender + entropy + payload)
|
||||
And the message should have an entropy stamp
|
||||
|
||||
Scenario: Gossip message types
|
||||
When I create a gossip message of type "<type>"
|
||||
Then the message type code should be <code>
|
||||
|
||||
Examples:
|
||||
| type | code |
|
||||
| trust_vouch | 0 |
|
||||
| trust_revoke | 1 |
|
||||
| reputation_update | 2 |
|
||||
| heartbeat | 3 |
|
||||
|
||||
# Probabilistic Flooding
|
||||
Scenario: Message propagation probability
|
||||
Given node "alpha" broadcasts a gossip message
|
||||
When the message reaches "beta"
|
||||
Then "beta" should forward with probability p = 0.7
|
||||
And the expected coverage after 3 hops should be > 80%
|
||||
|
||||
Scenario: Duplicate detection via message ID
|
||||
Given node "beta" has seen message ID 12345
|
||||
When "beta" receives message ID 12345 again
|
||||
Then "beta" should not forward the duplicate
|
||||
And "beta" should update the seen timestamp
|
||||
|
||||
# DAG Structure and Partition Detection
|
||||
Scenario: Build gossip DAG
|
||||
Given the following gossip sequence:
|
||||
| sender | refs |
|
||||
| alpha | [] |
|
||||
| beta | [alpha:1] |
|
||||
| gamma | [alpha:1, beta:1] |
|
||||
Then the DAG should have 3 nodes
|
||||
And "gamma" should have 2 incoming edges
|
||||
And the DAG depth should be 2
|
||||
|
||||
Scenario: Detect network partition via coverage
|
||||
Given the network has partitioned into [alpha, beta] and [gamma, delta]
|
||||
When "alpha" tracks gossip coverage
|
||||
And messages from "alpha" fail to reach "gamma" for 60 seconds
|
||||
Then "alpha" should report "low_coverage" anomaly
|
||||
And the anomaly score should be > 0.7
|
||||
|
||||
Scenario: Heal partition upon reconnection
|
||||
Given a partition exists between [alpha, beta] and [gamma]
|
||||
When the partition heals and "beta" reconnects to "gamma"
|
||||
Then "beta" should sync missing gossip messages
|
||||
And "gamma" should acknowledge receipt
|
||||
And the coverage anomaly should resolve
|
||||
|
||||
# Entropy and Replay Protection
|
||||
Scenario: Entropy stamp ordering
|
||||
Given message A with entropy 1000
|
||||
And message B with entropy 2000
|
||||
Then message B is newer than message A
|
||||
And a node should reject messages with entropy < last_seen - window
|
||||
|
||||
Scenario: Replay attack prevention
|
||||
Given node "alpha" has entropy window [1000, 2000]
|
||||
When "alpha" receives a message with entropy 500
|
||||
Then the message should be rejected as "stale"
|
||||
And "alpha" should not forward it
|
||||
|
||||
# Erasure Tolerance
|
||||
Scenario: Message loss tolerance
|
||||
Given a gossip DAG with k=3 references per message
|
||||
When 30% of messages are lost randomly
|
||||
Then the DAG should remain connected with > 95% probability
|
||||
And reconstruction should be possible via redundant paths
|
||||
|
||||
# Performance (Kenya Rule)
|
||||
Scenario: Gossip overhead
|
||||
Given a network with 1000 nodes
|
||||
When each node sends 1 message per minute
|
||||
Then the bandwidth per node should be < 10 KB/minute
|
||||
And the memory for gossip state should be < 1 MB
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
Feature: A* Trust Pathfinding
|
||||
As a Libertaria agent
|
||||
I need to find reputation-guided paths through the trust graph
|
||||
So that I can verify trust relationships efficiently
|
||||
|
||||
Background:
|
||||
Given a QVL database with the following trust topology:
|
||||
| from | to | level | risk | reputation |
|
||||
| alice | bob | 3 | -0.3 | 0.8 |
|
||||
| bob | charlie | 3 | -0.3 | 0.7 |
|
||||
| alice | dave | 3 | -0.3 | 0.9 |
|
||||
| dave | charlie | 3 | -0.3 | 0.6 |
|
||||
| bob | eve | 3 | -0.3 | 0.2 |
|
||||
|
||||
# Basic Pathfinding
|
||||
Scenario: Find shortest trust path
|
||||
When I search for a path from "alice" to "charlie"
|
||||
Then the path should be: "alice" → "bob" → "charlie"
|
||||
And the total cost should be approximately 0.6
|
||||
|
||||
Scenario: No path exists
|
||||
When I search for a path from "alice" to "frank"
|
||||
Then the path should be null
|
||||
And the result should indicate "no path found"
|
||||
|
||||
Scenario: Direct path preferred over indirect
|
||||
Given "alice" has direct trust level 7 to "charlie"
|
||||
When I search for a path from "alice" to "charlie"
|
||||
Then the path should be: "alice" → "charlie"
|
||||
And the path length should be 1
|
||||
|
||||
# Reputation-Guided Pathfinding
|
||||
Scenario: Reputation heuristic avoids low-reputation nodes
|
||||
When I search for a path from "alice" to "eve"
|
||||
Then the path should be: "alice" → "bob" → "eve"
|
||||
And the algorithm should penalize "bob" for low reputation (0.2)
|
||||
|
||||
Scenario: Zero heuristic degrades to Dijkstra
|
||||
When I search with zero heuristic from "alice" to "charlie"
|
||||
Then the result should be optimal (guaranteed shortest path)
|
||||
But the search should expand more nodes than with reputation heuristic
|
||||
|
||||
# Path Verification
|
||||
Scenario: Verify constructed path
|
||||
Given a path: "alice" → "bob" → "charlie"
|
||||
When I verify the path against the graph
|
||||
Then each edge in the path should exist
|
||||
And no edge should be expired
|
||||
And the path verification should succeed
|
||||
|
||||
Scenario: Verify path with expired edge
|
||||
Given a path: "alice" → "bob" → "charlie"
|
||||
And the edge "bob" → "charlie" has expired
|
||||
When I verify the path
|
||||
Then the verification should fail
|
||||
And the error should indicate "expired edge at hop 2"
|
||||
|
||||
# Proof-of-Path
|
||||
Scenario: Generate Proof-of-Path bundle
|
||||
Given a valid path: "alice" → "bob" → "charlie"
|
||||
When I generate a Proof-of-Path
|
||||
Then the PoP should contain all edge signatures
|
||||
And the PoP should be verifiable by any node
|
||||
And the PoP should have a timestamp and entropy stamp
|
||||
|
||||
Scenario: Verify Proof-of-Path
|
||||
Given a Proof-of-Path from "alice" to "charlie"
|
||||
When any node verifies the PoP
|
||||
Then the verification should succeed if all signatures are valid
|
||||
And the verification should fail if any signature is invalid
|
||||
|
||||
# Path Constraints
|
||||
Scenario: Maximum path depth
|
||||
When I search for a path with max_depth 2 from "alice" to "charlie"
|
||||
And the shortest path requires 3 hops
|
||||
Then the search should return null
|
||||
And indicate "max depth exceeded"
|
||||
|
||||
Scenario: Minimum trust threshold
|
||||
When I search for a path with minimum_trust_level 5
|
||||
And all edges have level 3
|
||||
Then no path should be found
|
||||
And the result should indicate "trust threshold not met"
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
Feature: Proof-of-Path Integration with Reputation
|
||||
As a Libertaria security validator
|
||||
I need to verify trust paths cryptographically
|
||||
And maintain reputation scores based on verification history
|
||||
So that trust decay reflects actual behavior
|
||||
|
||||
Background:
|
||||
Given a QVL database with established trust edges
|
||||
And a reputation map for all nodes
|
||||
|
||||
# Reputation Scoring
|
||||
Scenario: Initial neutral reputation
|
||||
Given a new node "frank" joins the network
|
||||
Then "frank"'s reputation score should be 0.5 (neutral)
|
||||
And total_checks should be 0
|
||||
|
||||
Scenario: Reputation increases with successful verification
|
||||
When node "alice" sends a PoP that verifies successfully
|
||||
Then "alice"'s reputation should increase
|
||||
And the increase should be damped (not immediate 1.0)
|
||||
And successful_checks should increment
|
||||
|
||||
Scenario: Reputation decreases with failed verification
|
||||
When node "bob" sends a PoP that fails verification
|
||||
Then "bob"'s reputation should decrease
|
||||
And the decrease should be faster than increases (asymmetry)
|
||||
And total_checks should increment
|
||||
|
||||
Scenario: Bayesian reputation update formula
|
||||
Given "charlie" has reputation 0.6 after 10 checks
|
||||
When a new verification succeeds
|
||||
Then the update should be: score = 0.7*0.6 + 0.3*(10/11)
|
||||
And the new score should be approximately 0.645
|
||||
|
||||
# Reputation Decay
|
||||
Scenario: Time-based reputation decay
|
||||
Given "alice" has reputation 0.8 from verification at time T
|
||||
When half_life time passes without new verification
|
||||
Then "alice"'s reputation should decay to ~0.4
|
||||
When another half_life passes
|
||||
Then reputation should decay to ~0.2
|
||||
|
||||
Scenario: Decay stops at minimum threshold
|
||||
Given "bob" has reputation 0.1 (low but not zero)
|
||||
When significant time passes
|
||||
Then "bob"'s reputation should not go below 0.05 (floor)
|
||||
|
||||
# PoP Verification Flow
|
||||
Scenario: Successful PoP verification
|
||||
Given a valid Proof-of-Path from "alice" to "charlie"
|
||||
When I verify against the expected receiver and sender
|
||||
Then the verdict should be "valid"
|
||||
And "alice"'s reputation should increase
|
||||
And the verification should be logged with entropy stamp
|
||||
|
||||
Scenario: Broken link in PoP
|
||||
Given a PoP with an edge that no longer exists
|
||||
When I verify the PoP
|
||||
Then the verdict should be "broken_link"
|
||||
And the specific broken edge should be identified
|
||||
And "alice"'s reputation should decrease
|
||||
|
||||
Scenario: Expired edge in PoP
|
||||
Given a PoP containing an expired trust edge
|
||||
When I verify the PoP
|
||||
Then the verdict should be "expired"
|
||||
And the expiration timestamp should be reported
|
||||
|
||||
Scenario: Invalid signature in PoP
|
||||
Given a PoP with a tampered signature
|
||||
When I verify the PoP
|
||||
Then the verdict should be "invalid_signature"
|
||||
And "alice"'s reputation should decrease significantly
|
||||
|
||||
# A* Heuristic Integration
|
||||
Scenario: Reputation-guided pathfinding
|
||||
Given "alice" has reputation 0.9
|
||||
And "bob" has reputation 0.3
|
||||
When searching for a path through either node
|
||||
Then the algorithm should prefer "alice" (higher reputation)
|
||||
And the path cost through "alice" should be lower
|
||||
|
||||
Scenario: Admissible heuristic guarantee
|
||||
Given any reputation configuration
|
||||
When using reputationHeuristic for A*
|
||||
Then the heuristic should never overestimate true cost
|
||||
And A* optimality should be preserved
|
||||
|
||||
# Low Reputation Handling
|
||||
Scenario: Identify low-reputation nodes
|
||||
Given nodes with reputations:
|
||||
| node | reputation |
|
||||
| alice | 0.9 |
|
||||
| bob | 0.2 |
|
||||
| charlie | 0.1 |
|
||||
When I query for nodes below threshold 0.3
|
||||
Then I should receive ["bob", "charlie"]
|
||||
|
||||
Scenario: Quarantine trigger
|
||||
Given "mallory" has reputation < 0.2 after 10+ checks
|
||||
When the low-reputation threshold is 0.2
|
||||
Then "mallory" should be flagged for quarantine review
|
||||
And future PoPs from "mallory" should be extra scrutinized
|
||||
|
||||
# Bulk Operations
|
||||
Scenario: Decay all reputations periodically
|
||||
Given 1000 nodes with various last_verified times
|
||||
When the daily decay job runs
|
||||
Then all reputations should be updated based on time since last verification
|
||||
And the operation should complete in < 100ms
|
||||
|
||||
Scenario: Populate RiskGraph from reputation
|
||||
Given a CompactTrustGraph with raw trust levels
|
||||
And a ReputationMap with scores
|
||||
When I populate the RiskGraph
|
||||
Then each edge risk should be calculated as (1 - reputation)
|
||||
And the RiskGraph should be ready for Bellman-Ford
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
Feature: QVL Trust Graph Core
|
||||
As a Libertaria node operator
|
||||
I need to manage trust relationships in a graph
|
||||
So that I can establish verifiable trust paths between agents
|
||||
|
||||
Background:
|
||||
Given a new QVL database is initialized
|
||||
And the following DIDs are registered:
|
||||
| did | alias |
|
||||
| did:alice:123 | alice |
|
||||
| did:bob:456 | bob |
|
||||
| did:charlie:789 | charlie |
|
||||
|
||||
# RiskGraph Basic Operations
|
||||
Scenario: Add trust edge between two nodes
|
||||
When "alice" grants trust level 3 to "bob"
|
||||
Then the graph should contain an edge from "alice" to "bob"
|
||||
And the edge should have trust level 3
|
||||
And "bob" should be in "alice"'s outgoing neighbors
|
||||
|
||||
Scenario: Remove trust edge
|
||||
Given "alice" has granted trust to "bob"
|
||||
When "alice" revokes trust from "bob"
|
||||
Then the edge from "alice" to "bob" should not exist
|
||||
And "bob" should not be in "alice"'s outgoing neighbors
|
||||
|
||||
Scenario: Query incoming trust edges
|
||||
Given "alice" has granted trust to "charlie"
|
||||
And "bob" has granted trust to "charlie"
|
||||
When I query incoming edges for "charlie"
|
||||
Then I should receive 2 edges
|
||||
And the edges should be from "alice" and "bob"
|
||||
|
||||
Scenario: Trust edge with TTL expiration
|
||||
When "alice" grants trust level 5 to "bob" with TTL 86400 seconds
|
||||
Then the edge should have an expiration timestamp
|
||||
And the edge should be valid immediately
|
||||
When 86401 seconds pass
|
||||
Then the edge should be expired
|
||||
And querying the edge should return null
|
||||
|
||||
# RiskEdge Properties
|
||||
Scenario Outline: Risk score calculation from trust level
|
||||
When "alice" grants trust level <level> to "bob"
|
||||
Then the risk score should be <risk>
|
||||
|
||||
Examples:
|
||||
| level | risk |
|
||||
| 7 | -1.0 |
|
||||
| 3 | -0.3 |
|
||||
| 0 | 0.0 |
|
||||
| -3 | 0.3 |
|
||||
| -7 | 1.0 |
|
||||
|
||||
Scenario: Edge metadata includes entropy stamp
|
||||
When "alice" grants trust to "bob" at entropy 1234567890
|
||||
Then the edge should have entropy stamp 1234567890
|
||||
And the edge should have a unique nonce
|
||||
|
||||
Scenario: Betrayal edge detection
|
||||
When "alice" grants trust level -7 to "bob"
|
||||
Then the edge should be marked as betrayal
|
||||
And the risk score should be positive
|
||||
|
|
@ -0,0 +1,338 @@
|
|||
# Libertaria SDK for Janus
|
||||
|
||||
> Sovereign; Kinetic; Anti-Fragile.
|
||||
|
||||
**Version:** 0.2.0-alpha
|
||||
**Status:** Sprint 2 Complete (GQL Parser + Codegen)
|
||||
**License:** MIT + Libertaria Commons Clause
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Libertaria SDK provides primitives for building sovereign agent networks on top of [Janus](https://github.com/janus-lang/janus) — the programming language designed for Carbon-Silicon symbiosis.
|
||||
|
||||
This SDK implements the **L1 Identity Layer** of the Libertaria Stack, featuring:
|
||||
|
||||
- **Cryptographic Identity** — Ed25519-based with rotation and burn capabilities
|
||||
- **Trust Graph** — QVL (Quasar Vector Lattice) engine with betrayal detection
|
||||
- **GQL (Graph Query Language)** — ISO/IEC 39075:2024 compliant query interface
|
||||
- **Persistent Storage** — libmdbx backend with Kenya Rule compliance (<10MB)
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ (Your Agent / libertaria.bot) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Libertaria SDK │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Identity │ │ Trust Graph │ │ GQL │ │
|
||||
│ │ (identity) │ │ (qvl) │ │ (gql/*.zig) │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Message │ │ Context │ │ Memory │ │
|
||||
│ │ (message) │ │ (context) │ │ (memory) │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Janus Standard Library │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Janus Compiler (:service) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Sovereign Identity
|
||||
|
||||
```janus
|
||||
import libertaria.{identity}
|
||||
|
||||
// Create a new sovereign identity
|
||||
let id = identity.create()
|
||||
|
||||
// Sign a message
|
||||
let msg = bytes.from_string("Hello Sovereigns!")
|
||||
let sig = identity.sign(id, msg)
|
||||
|
||||
// Verify signature
|
||||
assert identity.verify(id, msg, sig)
|
||||
|
||||
// Rotate identity (new keys, linked provenance)
|
||||
let (new_id, old_id) = identity.rotate(id)
|
||||
|
||||
// Burn identity (cryptographic deletion)
|
||||
let burned = identity.burn(id)
|
||||
```
|
||||
|
||||
### 2. Trust Graph (QVL)
|
||||
|
||||
```janus
|
||||
import libertaria.{qvl}
|
||||
|
||||
// Create hybrid graph (persistent + in-memory)
|
||||
let graph = qvl.HybridGraph.init(&persistent, allocator)
|
||||
|
||||
// Add trust edges
|
||||
graph.addEdge(.{
|
||||
from = alice,
|
||||
to = bob,
|
||||
risk = -0.3, // Negative = trust
|
||||
level = 3, // Trust level 1-7
|
||||
timestamp = now(),
|
||||
expires_at = now() + duration.days(30)
|
||||
})
|
||||
|
||||
// Detect betrayal rings (negative cycles)
|
||||
let result = try graph.detectBetrayal(alice)
|
||||
if result.betrayal_cycles.items.len > 0 {
|
||||
// Handle betrayal
|
||||
}
|
||||
|
||||
// Find trust path
|
||||
let path = try graph.findTrustPath(alice, charlie,
|
||||
heuristic = qvl.reputationHeuristic,
|
||||
heuristic_ctx = &rep_map)
|
||||
```
|
||||
|
||||
### 3. GQL (Graph Query Language)
|
||||
|
||||
```janus
|
||||
import libertaria.{gql}
|
||||
|
||||
// Parse GQL query
|
||||
let query_str = "MATCH (n:Identity)-[t:TRUST]->(m) WHERE n.did = 'alice' RETURN m"
|
||||
let query = try gql.parse(allocator, query_str)
|
||||
defer query.deinit()
|
||||
|
||||
// Transpile to Zig code
|
||||
let zig_code = try gql.generateZig(allocator, query)
|
||||
defer allocator.free(zig_code)
|
||||
|
||||
// Generated code looks like:
|
||||
// pub fn execute(graph: *qvl.HybridGraph) !void {
|
||||
// // MATCH statement
|
||||
// // Traverse from n
|
||||
// var t = try graph.getOutgoing(n);
|
||||
// // Filter by type: TRUST
|
||||
// var m = t.to;
|
||||
// // WHERE n.did == "alice"
|
||||
// // RETURN statement
|
||||
// var results = std.ArrayList(Result).init(allocator);
|
||||
// defer results.deinit();
|
||||
// try results.append(m);
|
||||
// }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Reference
|
||||
|
||||
### `libertaria.identity`
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `create()` | Generate new Ed25519 identity |
|
||||
| `rotate(id)` | Rotate keys with provenance chain |
|
||||
| `burn(id)` | Cryptographic deletion |
|
||||
| `sign(id, msg)` | Sign message |
|
||||
| `verify(id, msg, sig)` | Verify signature |
|
||||
| `is_valid(id)` | Check not revoked/expired |
|
||||
|
||||
### `libertaria.qvl`
|
||||
|
||||
| Type | Purpose |
|
||||
|------|---------|
|
||||
| `HybridGraph` | Persistent + in-memory graph |
|
||||
| `PersistentGraph` | libmdbx-backed storage |
|
||||
| `RiskGraph` | In-memory graph for algorithms |
|
||||
| `GraphTransaction` | Batch operations |
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `detectBetrayal(source)` | Bellman-Ford negative cycle detection |
|
||||
| `findTrustPath(src, tgt, heuristic)` | A* pathfinding |
|
||||
| `addEdge(edge)` | Add trust edge |
|
||||
| `getOutgoing(node)` | Get neighbors |
|
||||
|
||||
### `libertaria.gql`
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `parse(allocator, query)` | Parse GQL string to AST |
|
||||
| `generateZig(allocator, query)` | Transpile to Zig code |
|
||||
|
||||
---
|
||||
|
||||
## GQL Syntax
|
||||
|
||||
### MATCH — Pattern Matching
|
||||
|
||||
```gql
|
||||
-- Simple node
|
||||
MATCH (n:Identity)
|
||||
|
||||
-- Node with properties
|
||||
MATCH (n:Identity {did: 'alice', active: true})
|
||||
|
||||
-- One-hop traversal
|
||||
MATCH (a)-[t:TRUST]->(b)
|
||||
|
||||
-- Variable-length path
|
||||
MATCH (a)-[t:TRUST*1..3]->(b)
|
||||
|
||||
-- With WHERE clause
|
||||
MATCH (n:Identity)-[t:TRUST]->(m)
|
||||
WHERE n.did = 'alice' AND t.level >= 3
|
||||
RETURN m
|
||||
```
|
||||
|
||||
### CREATE — Insert Data
|
||||
|
||||
```gql
|
||||
-- Create node
|
||||
CREATE (n:Identity {did: 'alice'})
|
||||
|
||||
-- Create edge
|
||||
CREATE (a)-[t:TRUST {level: 3}]->(b)
|
||||
|
||||
-- Create pattern
|
||||
CREATE (a:Identity)-[t:TRUST]->(b:Identity)
|
||||
```
|
||||
|
||||
### DELETE — Remove Data
|
||||
|
||||
```gql
|
||||
-- Delete nodes
|
||||
MATCH (n:Identity)
|
||||
WHERE n.did = 'compromised'
|
||||
DELETE n
|
||||
```
|
||||
|
||||
### RETURN — Project Results
|
||||
|
||||
```gql
|
||||
-- Return variable
|
||||
MATCH (n) RETURN n
|
||||
|
||||
-- Return multiple
|
||||
MATCH (a)-[t]->(b) RETURN a, t, b
|
||||
|
||||
-- With alias
|
||||
MATCH (n) RETURN n.did AS identity
|
||||
|
||||
-- Aggregations (planned)
|
||||
MATCH (n) RETURN count(n) AS total
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Exit is Voice
|
||||
|
||||
Agents can leave, taking their data cryptographically:
|
||||
|
||||
```janus
|
||||
// Burn identity
|
||||
let burned = identity.burn(my_id)
|
||||
// After burn: no new signatures possible
|
||||
// Verification of historical signatures still works
|
||||
```
|
||||
|
||||
### 2. Profit = Honesty
|
||||
|
||||
Economic stakes align incentives:
|
||||
|
||||
- **Posting** requires $SCRAP burn
|
||||
- **Identity** requires $STASIS bond
|
||||
- **Reputation** decays without verification
|
||||
|
||||
### 3. Code is Law
|
||||
|
||||
No central moderation, only protocol rules:
|
||||
|
||||
- **Betrayal detection** via Bellman-Ford (mathematical, not subjective)
|
||||
- **Path verification** via cryptographic proofs
|
||||
- **Reputation** via Bayesian updates
|
||||
|
||||
### 4. Kenya Compliance
|
||||
|
||||
Resource-constrained environments:
|
||||
|
||||
- **Binary size:** <200KB for L1
|
||||
- **Memory:** <10MB for graph operations
|
||||
- **Storage:** Single-file embedded (libmdbx)
|
||||
- **No cloud calls:** Fully offline-capable
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all SDK tests
|
||||
zig build test-qvl
|
||||
|
||||
# Run specific module
|
||||
zig build test -- --module lexer
|
||||
|
||||
# Run with coverage (planned)
|
||||
zig build test-qvl-coverage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Sprint 0 ✅ — BDD Specifications
|
||||
- 58 Gherkin scenarios for QVL
|
||||
|
||||
### Sprint 1 ✅ — Storage Layer
|
||||
- libmdbx PersistentGraph
|
||||
- HybridGraph (disk + memory)
|
||||
|
||||
### Sprint 2 ✅ — GQL Parser
|
||||
- ISO/IEC 39075:2024 compliant
|
||||
- Lexer, Parser, AST, Codegen
|
||||
|
||||
### Sprint 3 🔄 — Documentation
|
||||
- API reference (this file)
|
||||
- Architecture decision records
|
||||
- Tutorial: Building your first agent
|
||||
|
||||
### Sprint 4 📅 — L4 Feed
|
||||
- DuckDB integration
|
||||
- LanceDB vector store
|
||||
- Social media primitives
|
||||
|
||||
### Sprint 5 📅 — Production
|
||||
- Performance benchmarks
|
||||
- Security audit
|
||||
- Release v1.0
|
||||
|
||||
---
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [Janus Language](https://github.com/janus-lang/janus) — The foundation
|
||||
- [Libertaria Stack](https://git.maiwald.work/Libertaria) — Full protocol implementation
|
||||
- [Moltbook](https://moltbook.com) — Agent social network (lessons learned)
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License + Libertaria Commons Clause
|
||||
|
||||
See LICENSE for details.
|
||||
|
||||
---
|
||||
|
||||
*Forge burns bright. The Exit is being built.*
|
||||
|
||||
⚡️
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
-- libertaria/context.jan
|
||||
-- NCP (Nexus Context Protocol) implementation
|
||||
-- Structured, hierarchical context management for agent conversations
|
||||
|
||||
module Context exposing
|
||||
( Context
|
||||
, create, fork, merge, close
|
||||
, current_depth, max_depth
|
||||
, add_message, get_messages
|
||||
, subscribe, unsubscribe
|
||||
, to_astdb_query
|
||||
)
|
||||
|
||||
import message.{Message}
|
||||
import memory.{VectorStore}
|
||||
import time.{timestamp}
|
||||
|
||||
-- Context is a structured conversation container
|
||||
type Context =
|
||||
{ id: context_id.ContextId
|
||||
, parent: ?context_id.ContextId -- Hierarchical nesting
|
||||
, depth: int -- Nesting level (prevents infinite loops)
|
||||
, created_at: timestamp.Timestamp
|
||||
, messages: list.List(Message)
|
||||
, metadata: metadata.ContextMetadata
|
||||
, vector_store: ?VectorStore -- Semantic indexing
|
||||
, subscribers: set.Set(fingerprint.Fingerprint)
|
||||
, closed: bool
|
||||
}
|
||||
|
||||
type ContextConfig =
|
||||
{ max_depth: int = 100 -- Max nesting before forced flattening
|
||||
, max_messages: int = 10000 -- Auto-archive older messages
|
||||
, enable_vector_index: bool = true
|
||||
, retention_policy: RetentionPolicy
|
||||
}
|
||||
|
||||
type RetentionPolicy =
|
||||
| Keep_Forever
|
||||
| Auto_Archive_After(duration.Duration)
|
||||
| Delete_After(duration.Duration)
|
||||
|
||||
-- Create root context
|
||||
-- Top-level conversation container
|
||||
fn create(config: ContextConfig) -> Context
|
||||
let id = context_id.generate()
|
||||
let now = timestamp.now()
|
||||
let vs = if config.enable_vector_index
|
||||
then some(memory.create_vector_store())
|
||||
else null
|
||||
|
||||
{ id = id
|
||||
, parent = null
|
||||
, depth = 0
|
||||
, created_at = now
|
||||
, messages = list.empty()
|
||||
, metadata = metadata.create(id)
|
||||
, vector_store = vs
|
||||
, subscribers = set.empty()
|
||||
, closed = false
|
||||
}
|
||||
|
||||
-- Fork child context from parent
|
||||
-- Used for: sub-conversations, branching decisions, isolated experiments
|
||||
fn fork(parent: Context, reason: string, config: ContextConfig) -> result.Result(Context, error.ForkError)
|
||||
if parent.depth >= config.max_depth then
|
||||
error.err(MaxDepthExceeded)
|
||||
else if parent.closed then
|
||||
error.err(ParentClosed)
|
||||
else
|
||||
let id = context_id.generate()
|
||||
let now = timestamp.now()
|
||||
let vs = if config.enable_vector_index
|
||||
then some(memory.create_vector_store())
|
||||
else null
|
||||
|
||||
let child =
|
||||
{ id = id
|
||||
, parent = some(parent.id)
|
||||
, depth = parent.depth + 1
|
||||
, created_at = now
|
||||
, messages = list.empty()
|
||||
, metadata = metadata.create(id)
|
||||
|> metadata.set_parent(parent.id)
|
||||
|> metadata.set_fork_reason(reason)
|
||||
, vector_store = vs
|
||||
, subscribers = set.empty()
|
||||
, closed = false
|
||||
}
|
||||
|
||||
ok(child)
|
||||
|
||||
-- Merge child context back into parent
|
||||
-- Consolidates messages, preserves fork history
|
||||
fn merge(child: Context, into parent: Context) -> result.Result(Context, error.MergeError)
|
||||
if child.parent != some(parent.id) then
|
||||
error.err(NotMyParent)
|
||||
else if child.closed then
|
||||
error.err(ChildClosed)
|
||||
else
|
||||
let merged_messages = parent.messages ++ child.messages
|
||||
let merged_subs = set.union(parent.subscribers, child.subscribers)
|
||||
|
||||
let updated_parent =
|
||||
{ parent with
|
||||
messages = merged_messages
|
||||
, subscribers = merged_subs
|
||||
, metadata = parent.metadata
|
||||
|> metadata.add_merge_history(child.id, child.messages.length())
|
||||
}
|
||||
|
||||
ok(updated_parent)
|
||||
|
||||
-- Close context (final state)
|
||||
-- No more messages, preserves history
|
||||
fn close(ctx: Context) -> Context
|
||||
{ ctx with closed = true }
|
||||
|
||||
-- Get current nesting depth
|
||||
fn current_depth(ctx: Context) -> int
|
||||
ctx.depth
|
||||
|
||||
-- Get max allowed depth (from config)
|
||||
fn max_depth(ctx: Context) -> int
|
||||
ctx.metadata.config.max_depth
|
||||
|
||||
-- Add message to context
|
||||
-- Indexes in vector store if enabled
|
||||
fn add_message(ctx: Context, msg: Message) -> result.Result(Context, error.AddError)
|
||||
if ctx.closed then
|
||||
error.err(ContextClosed)
|
||||
else
|
||||
let updated = { ctx with messages = ctx.messages ++ [msg] }
|
||||
|
||||
-- Index in vector store for semantic search
|
||||
match ctx.vector_store with
|
||||
| null -> ok(updated)
|
||||
| some(vs) ->
|
||||
let embedding = memory.embed(message.content(msg))
|
||||
let indexed_vs = memory.store(vs, message.id(msg), embedding)
|
||||
ok({ updated with vector_store = some(indexed_vs) })
|
||||
|
||||
-- Get all messages in context
|
||||
fn get_messages(ctx: Context) -> list.List(Message)
|
||||
ctx.messages
|
||||
|
||||
-- Subscribe agent to context updates
|
||||
fn subscribe(ctx: Context, agent: fingerprint.Fingerprint) -> Context
|
||||
{ ctx with subscribers = set.insert(ctx.subscribers, agent) }
|
||||
|
||||
-- Unsubscribe agent
|
||||
fn unsubscribe(ctx: Context, agent: fingerprint.Fingerprint) -> Context
|
||||
{ ctx with subscribers = set.remove(ctx.subscribers, agent) }
|
||||
|
||||
-- Convert to ASTDB query for semantic search
|
||||
-- Enables: "Find similar contexts", "What did we discuss about X?"
|
||||
fn to_astdb_query(ctx: Context) -> astdb.Query
|
||||
let message_hashes = list.map(ctx.messages, message.hash)
|
||||
let time_range =
|
||||
{ start = ctx.created_at
|
||||
, end = match list.last(ctx.messages) with
|
||||
| null -> timestamp.now()
|
||||
| some(last_msg) -> message.timestamp(last_msg)
|
||||
}
|
||||
|
||||
astdb.query()
|
||||
|> astdb.with_context_id(ctx.id)
|
||||
|> astdb.with_message_hashes(message_hashes)
|
||||
|> astdb.with_time_range(time_range)
|
||||
|> astdb.with_depth(ctx.depth)
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
-- libertaria/identity.jan
|
||||
-- Cryptographic identity for sovereign agents
|
||||
-- Exit is Voice: Identity can be rotated, expired, or burned
|
||||
|
||||
module Identity exposing
|
||||
( Identity
|
||||
, create, rotate, burn
|
||||
, is_valid, is_expired
|
||||
, public_key, fingerprint
|
||||
, sign, verify
|
||||
)
|
||||
|
||||
import crypto.{ed25519, hash}
|
||||
import time.{timestamp, duration}
|
||||
|
||||
-- Core identity type with cryptographic material and metadata
|
||||
type Identity =
|
||||
{ public_key: ed25519.PublicKey
|
||||
, secret_key: ed25519.SecretKey -- Encrypted at rest
|
||||
, created_at: timestamp.Timestamp
|
||||
, expires_at: ?timestamp.Timestamp -- Optional expiry
|
||||
, rotated_from: ?fingerprint.Fingerprint -- Chain of custody
|
||||
, revoked: bool
|
||||
}
|
||||
|
||||
-- Create new sovereign identity
|
||||
-- Fresh keypair, no history, self-sovereign
|
||||
fn create() -> Identity
|
||||
let (pk, sk) = ed25519.generate_keypair()
|
||||
let now = timestamp.now()
|
||||
|
||||
{ public_key = pk
|
||||
, secret_key = sk
|
||||
, created_at = now
|
||||
, expires_at = null
|
||||
, rotated_from = null
|
||||
, revoked = false
|
||||
}
|
||||
|
||||
-- Rotate identity: New keys, linked provenance
|
||||
-- Old identity becomes invalid after grace period
|
||||
fn rotate(old: Identity) -> (Identity, Identity)
|
||||
assert not old.revoked "Cannot rotate revoked identity"
|
||||
|
||||
let (new_pk, new_sk) = ed25519.generate_keypair()
|
||||
let now = timestamp.now()
|
||||
let old_fp = fingerprint.of_identity(old)
|
||||
|
||||
let new_id =
|
||||
{ public_key = new_pk
|
||||
, secret_key = new_sk
|
||||
, created_at = now
|
||||
, expires_at = null
|
||||
, rotated_from = some(old_fp)
|
||||
, revoked = false
|
||||
}
|
||||
|
||||
-- Old identity gets short grace period then auto-expires
|
||||
let grace_period = duration.hours(24)
|
||||
let expired_old = { old with expires_at = some(now + grace_period) }
|
||||
|
||||
(new_id, expired_old)
|
||||
|
||||
-- Burn identity: Cryptographic deletion
|
||||
-- After burn, no messages can be signed, verification still works for history
|
||||
fn burn(id: Identity) -> Identity
|
||||
{ id with
|
||||
secret_key = ed25519.zero_secret(id.secret_key)
|
||||
, revoked = true
|
||||
, expires_at = some(timestamp.now())
|
||||
}
|
||||
|
||||
-- Check if identity is currently valid
|
||||
fn is_valid(id: Identity) -> bool
|
||||
not id.revoked and not is_expired(id)
|
||||
|
||||
-- Check if identity has expired
|
||||
fn is_expired(id: Identity) -> bool
|
||||
match id.expires_at with
|
||||
| null -> false
|
||||
| some(t) -> timestamp.now() > t
|
||||
|
||||
-- Get public key for sharing/verification
|
||||
fn public_key(id: Identity) -> ed25519.PublicKey
|
||||
id.public_key
|
||||
|
||||
-- Get fingerprint (short, unique identifier)
|
||||
fn fingerprint(id: Identity) -> fingerprint.Fingerprint
|
||||
fingerprint.of_key(id.public_key)
|
||||
|
||||
-- Sign message with this identity
|
||||
fn sign(id: Identity, message: bytes.Bytes) -> signature.Signature
|
||||
assert is_valid(id) "Cannot sign with invalid identity"
|
||||
ed25519.sign(id.secret_key, message)
|
||||
|
||||
-- Verify signature against this identity's public key
|
||||
fn verify(id: Identity, message: bytes.Bytes, sig: signature.Signature) -> bool
|
||||
ed25519.verify(id.public_key, message, sig)
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
-- libertaria/lib.jan
|
||||
-- Main entry point for Libertaria SDK
|
||||
-- Sovereign; Kinetic; Anti-Fragile.
|
||||
|
||||
module Libertaria exposing
|
||||
( -- Identity
|
||||
identity.Identity
|
||||
, identity.create, identity.rotate, identity.burn
|
||||
, identity.is_valid, identity.is_expired
|
||||
, identity.public_key, identity.fingerprint
|
||||
, identity.sign, identity.verify
|
||||
|
||||
-- Message
|
||||
, message.Message
|
||||
, message.create, message.create_reply
|
||||
, message.sender, message.content, message.timestamp
|
||||
, message.verify, message.is_authentic
|
||||
, message.to_bytes, message.from_bytes
|
||||
, message.hash, message.id
|
||||
|
||||
-- Context (NCP)
|
||||
, context.Context
|
||||
, context.create, context.fork, context.merge, context.close
|
||||
, context.current_depth, context.max_depth
|
||||
, context.add_message, context.get_messages
|
||||
, context.subscribe, context.unsubscribe
|
||||
, context.to_astdb_query
|
||||
|
||||
-- Memory
|
||||
, memory.VectorStore
|
||||
, memory.create_vector_store
|
||||
, memory.store, memory.retrieve, memory.search
|
||||
, memory.embed
|
||||
, memory.sync, memory.export, memory.import
|
||||
)
|
||||
|
||||
import identity
|
||||
import message
|
||||
import context
|
||||
import memory
|
||||
|
||||
-- SDK Version
|
||||
const VERSION = "0.1.0-alpha"
|
||||
const COMPATIBLE_JANUS_VERSION = ">= 1.0.0"
|
||||
|
||||
-- Quick-start: Create sovereign agent with full stack
|
||||
fn create_sovereign_agent() -> SovereignAgent
|
||||
let id = identity.create()
|
||||
let root_context = context.create({})
|
||||
let memory_store = memory.create_vector_store()
|
||||
|
||||
{ identity = id
|
||||
, root_context = root_context
|
||||
, memory = memory_store
|
||||
, version = VERSION
|
||||
}
|
||||
|
||||
type SovereignAgent =
|
||||
{ identity: identity.Identity
|
||||
, root_context: context.Context
|
||||
, memory: memory.VectorStore
|
||||
, version: string
|
||||
}
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
-- libertaria/memory.jan
|
||||
-- Semantic memory with VectorDB (LanceDB) integration
|
||||
-- Agents remember context through embeddings, not just raw logs
|
||||
|
||||
module Memory exposing
|
||||
( VectorStore
|
||||
, create_vector_store
|
||||
, store, retrieve, search
|
||||
, embed -- Uses Janus neuro module
|
||||
, sync, export, import
|
||||
)
|
||||
|
||||
import neuro.{embedding}
|
||||
import serde.{lance}
|
||||
import time.{timestamp}
|
||||
|
||||
-- Vector store configuration
|
||||
type VectorStore =
|
||||
{ uri: string -- LanceDB connection URI
|
||||
, dimension: int -- Embedding dimension (e.g., 768 for BERT, 1536 for OpenAI)
|
||||
, metric: DistanceMetric
|
||||
, table: lance.Table
|
||||
, cache: lru.Cache(vector_id.VectorId, embedding.Embedding)
|
||||
}
|
||||
|
||||
type DistanceMetric =
|
||||
| Cosine -- Best for semantic similarity
|
||||
| Euclidean -- Best for geometric distance
|
||||
| DotProduct -- Fastest, good for normalized embeddings
|
||||
|
||||
-- Default configuration for agent memory
|
||||
fn default_config() -> { dimension: 1536, metric: Cosine }
|
||||
|
||||
-- Create new vector store
|
||||
-- If uri points to existing store, opens it; otherwise creates new
|
||||
fn create_vector_store
|
||||
( uri: string = "memory.lance"
|
||||
, config: { dimension: int, metric: DistanceMetric } = default_config()
|
||||
) -> VectorStore
|
||||
|
||||
let table = lance.connect(uri)
|
||||
|> lance.create_table("embeddings")
|
||||
|> lance.with_vector_column("embedding", config.dimension)
|
||||
|> lance.with_metric(config.metric)
|
||||
|> lance.with_columns
|
||||
[ { name = "content_hash", type = "string" }
|
||||
, { name = "content_type", type = "string" }
|
||||
, { name = "created_at", type = "timestamp" }
|
||||
, { name = "context_id", type = "string", nullable = true }
|
||||
, { name = "metadata", type = "json", nullable = true }
|
||||
]
|
||||
|> lance.execute()
|
||||
|
||||
{ uri = uri
|
||||
, dimension = config.dimension
|
||||
, metric = config.metric
|
||||
, table = table
|
||||
, cache = lru.create(max_size = 1000)
|
||||
}
|
||||
|
||||
-- Generate embedding from content
|
||||
-- Uses Janus neuro module for local inference
|
||||
fn embed(content: bytes.Bytes, model: ?string = null) -> embedding.Embedding
|
||||
let content_str = bytes.to_string(content)
|
||||
neuro.embed(content_str, model = model)
|
||||
|
||||
-- Store embedding in vector database
|
||||
fn store
|
||||
( vs: VectorStore
|
||||
, id: vector_id.VectorId
|
||||
, emb: embedding.Embedding
|
||||
, content_hash: string -- Blake3 hash of original content
|
||||
, content_type: string = "text"
|
||||
, context_id: ?string = null
|
||||
, metadata: ?json.Json = null
|
||||
) -> VectorStore
|
||||
|
||||
let record =
|
||||
{ id = id
|
||||
, embedding = emb
|
||||
, content_hash = content_hash
|
||||
, content_type = content_type
|
||||
, created_at = timestamp.now()
|
||||
, context_id = context_id
|
||||
, metadata = metadata
|
||||
}
|
||||
|
||||
lance.insert(vs.table, record)
|
||||
|
||||
-- Update cache
|
||||
let new_cache = lru.put(vs.cache, id, emb)
|
||||
{ vs with cache = new_cache }
|
||||
|
||||
-- Retrieve exact embedding by ID
|
||||
fn retrieve(vs: VectorStore, id: vector_id.VectorId) -> ?embedding.Embedding
|
||||
-- Check cache first
|
||||
match lru.get(vs.cache, id) with
|
||||
| some(emb) -> some(emb)
|
||||
| null ->
|
||||
-- Query LanceDB
|
||||
let results = lance.query(vs.table)
|
||||
|> lance.where("id = ", id)
|
||||
|> lance.limit(1)
|
||||
|> lance.execute()
|
||||
|
||||
match list.head(results) with
|
||||
| null -> null
|
||||
| some(record) -> some(record.embedding)
|
||||
|
||||
-- Semantic search: Find similar embeddings
|
||||
fn search
|
||||
( vs: VectorStore
|
||||
, query_embedding: embedding.Embedding
|
||||
, top_k: int = 10
|
||||
, filter: ?string = null -- Optional SQL filter
|
||||
) -> list.SearchResult
|
||||
|
||||
let base_query = lance.query(vs.table)
|
||||
|> lance.nearest_neighbors("embedding", query_embedding)
|
||||
|> lance.limit(top_k)
|
||||
|
||||
let filtered_query = match filter with
|
||||
| null -> base_query
|
||||
| some(f) -> base_query |> lance.where(f)
|
||||
|
||||
lance.execute(filtered_query)
|
||||
|> list.map(fn r ->
|
||||
{ id = r.id
|
||||
, score = r.distance -- Lower is better for cosine/euclidean
|
||||
, content_hash = r.content_hash
|
||||
, content_type = r.content_type
|
||||
, created_at = r.created_at
|
||||
, context_id = r.context_id
|
||||
, metadata = r.metadata
|
||||
}
|
||||
)
|
||||
|
||||
-- Sync to disk (ensure durability)
|
||||
fn sync(vs: VectorStore) -> result.Result((), error.SyncError)
|
||||
lance.flush(vs.table)
|
||||
|
||||
-- Export to portable format
|
||||
fn export(vs: VectorStore, path: string) -> result.Result((), error.ExportError)
|
||||
lance.backup(vs.table, path)
|
||||
|
||||
-- Import from portable format
|
||||
fn import(path: string) -> result.Result(VectorStore, error.ImportError)
|
||||
let restored = lance.restore(path)
|
||||
ok(create_vector_store(uri = restored.uri))
|
||||
|
||||
-- Advanced: Hybrid search combining semantic + keyword
|
||||
type HybridResult =
|
||||
{ semantic_results: list.SearchResult
|
||||
, keyword_results: list.SearchResult
|
||||
, combined: list.SearchResult
|
||||
, reranking_score: float
|
||||
}
|
||||
|
||||
fn hybrid_search
|
||||
( vs: VectorStore
|
||||
, query_embedding: embedding.Embedding
|
||||
, query_text: string
|
||||
, top_k: int = 10
|
||||
, semantic_weight: float = 0.7
|
||||
) -> HybridResult
|
||||
|
||||
let semantic = search(vs, query_embedding, top_k * 2)
|
||||
let keyword = lance.full_text_search(vs.table, query_text, top_k * 2)
|
||||
|
||||
-- Reciprocal Rank Fusion for combining
|
||||
let combined = reciprocal_rank_fusion(semantic, keyword, semantic_weight)
|
||||
|
||||
{ semantic_results = list.take(semantic, top_k)
|
||||
, keyword_results = list.take(keyword, top_k)
|
||||
, combined = list.take(combined, top_k)
|
||||
, reranking_score = 0.0 -- Placeholder for cross-encoder reranking
|
||||
}
|
||||
|
||||
-- Internal: Reciprocal Rank Fusion algorithm
|
||||
fn reciprocal_rank_fusion
|
||||
( semantic: list.SearchResult
|
||||
, keyword: list.SearchResult
|
||||
, semantic_weight: float
|
||||
) -> list.SearchResult
|
||||
|
||||
let k = 60.0 -- RRF constant
|
||||
|
||||
let score_map = map.empty()
|
||||
|
||||
-- Score semantic results
|
||||
list.foreach_with_index(semantic, fn r, idx ->
|
||||
let rank = idx + 1
|
||||
let score = semantic_weight * (1.0 / (k + rank))
|
||||
score_map[r.id] = map.get_or_default(score_map, r.id, 0.0) + score
|
||||
)
|
||||
|
||||
-- Score keyword results
|
||||
list.foreach_with_index(keyword, fn r, idx ->
|
||||
let rank = idx + 1
|
||||
let score = (1.0 - semantic_weight) * (1.0 / (k + rank))
|
||||
score_map[r.id] = map.get_or_default(score_map, r.id, 0.0) + score
|
||||
)
|
||||
|
||||
-- Sort by combined score
|
||||
map.to_list(score_map)
|
||||
|> list.sort_by(fn (id, score) -> score, descending = true)
|
||||
|> list.map(fn (id, _) -> id)
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
-- libertaria/message.jan
|
||||
-- Signed, tamper-proof messages between agents
|
||||
-- Messages are immutable once created, cryptographically bound to sender
|
||||
|
||||
module Message exposing
|
||||
( Message
|
||||
, create, create_reply
|
||||
, sender, content, timestamp
|
||||
, verify, is_authentic
|
||||
, to_bytes, from_bytes
|
||||
, hash, id
|
||||
)
|
||||
|
||||
import identity.{Identity}
|
||||
import time.{timestamp}
|
||||
import crypto.{hash, signature}
|
||||
import serde.{msgpack}
|
||||
|
||||
-- A message is a signed envelope with content and metadata
|
||||
type Message =
|
||||
{ version: int -- Protocol version (for migration)
|
||||
, id: message_id.MessageId -- Content-addressed ID
|
||||
, parent: ?message_id.MessageId -- For threads/replies
|
||||
, sender: fingerprint.Fingerprint
|
||||
, content_type: ContentType
|
||||
, content: bytes.Bytes -- Opaque payload
|
||||
, created_at: timestamp.Timestamp
|
||||
, signature: signature.Signature
|
||||
}
|
||||
|
||||
type ContentType =
|
||||
| Text
|
||||
| Binary
|
||||
| Json
|
||||
| Janus_Ast
|
||||
| Encrypted -- Content is encrypted for specific recipient(s)
|
||||
|
||||
-- Create a new signed message
|
||||
-- Cryptographically binds content to sender identity
|
||||
fn create
|
||||
( from: Identity
|
||||
, content_type: ContentType
|
||||
, content: bytes.Bytes
|
||||
, parent: ?message_id.MessageId = null
|
||||
) -> Message
|
||||
|
||||
let now = timestamp.now()
|
||||
let sender_fp = identity.fingerprint(from)
|
||||
|
||||
-- Content-addressed ID: hash of content + metadata (before signing)
|
||||
let preliminary =
|
||||
{ version = 1
|
||||
, id = message_id.zero() -- Placeholder
|
||||
, parent = parent
|
||||
, sender = sender_fp
|
||||
, content_type = content_type
|
||||
, content = content
|
||||
, created_at = now
|
||||
, signature = signature.zero()
|
||||
}
|
||||
|
||||
let msg_id = compute_id(preliminary)
|
||||
let to_sign = serialize_for_signing({ preliminary with id = msg_id })
|
||||
let sig = identity.sign(from, to_sign)
|
||||
|
||||
{ preliminary with
|
||||
id = msg_id
|
||||
, signature = sig
|
||||
}
|
||||
|
||||
-- Create a reply to an existing message
|
||||
-- Maintains thread structure
|
||||
fn create_reply
|
||||
( from: Identity
|
||||
, to: Message
|
||||
, content_type: ContentType
|
||||
, content: bytes.Bytes
|
||||
) -> Message
|
||||
create(from, content_type, content, parent = some(to.id))
|
||||
|
||||
-- Get sender fingerprint
|
||||
fn sender(msg: Message) -> fingerprint.Fingerprint
|
||||
msg.sender
|
||||
|
||||
-- Get content
|
||||
fn content(msg: Message) -> bytes.Bytes
|
||||
msg.content
|
||||
|
||||
-- Get timestamp
|
||||
fn timestamp(msg: Message) -> timestamp.Timestamp
|
||||
msg.created_at
|
||||
|
||||
-- Verify message authenticity
|
||||
-- Checks: signature valid, sender identity not revoked
|
||||
fn verify(msg: Message, sender_id: Identity) -> bool
|
||||
let to_verify = serialize_for_signing(msg)
|
||||
identity.verify(sender_id, to_verify, msg.signature)
|
||||
|
||||
-- Quick check without full identity lookup
|
||||
-- Just verifies signature format and version
|
||||
fn is_authentic(msg: Message) -> bool
|
||||
msg.version == 1 and
|
||||
msg.signature != signature.zero() and
|
||||
msg.id == compute_id(msg)
|
||||
|
||||
-- Serialize to bytes for wire transfer
|
||||
fn to_bytes(msg: Message) -> bytes.Bytes
|
||||
msgpack.serialize(msg)
|
||||
|
||||
-- Deserialize from bytes
|
||||
fn from_bytes(data: bytes.Bytes) -> result.Result(Message, error.DeserializeError)
|
||||
msgpack.deserialize(data)
|
||||
|
||||
-- Get content hash (for deduplication, indexing)
|
||||
fn hash(msg: Message) -> hash.Hash
|
||||
crypto.blake3(msg.content)
|
||||
|
||||
-- Get message ID
|
||||
fn id(msg: Message) -> message_id.MessageId
|
||||
msg.id
|
||||
|
||||
-- Internal: Compute content-addressed ID
|
||||
fn compute_id(msg: Message) -> message_id.MessageId
|
||||
let canonical =
|
||||
{ version = msg.version
|
||||
, parent = msg.parent
|
||||
, sender = msg.sender
|
||||
, content_type = msg.content_type
|
||||
, content = msg.content
|
||||
, created_at = msg.created_at
|
||||
}
|
||||
message_id.from_hash(crypto.blake3(msgpack.serialize(canonical)))
|
||||
|
||||
-- Internal: Serialize for signing (excludes signature itself)
|
||||
fn serialize_for_signing(msg: Message) -> bytes.Bytes
|
||||
msgpack.serialize
|
||||
{ version = msg.version
|
||||
, id = msg.id
|
||||
, parent = msg.parent
|
||||
, sender = msg.sender
|
||||
, content_type = msg.content_type
|
||||
, content = msg.content
|
||||
, created_at = msg.created_at
|
||||
}
|
||||
|
|
@ -261,7 +261,7 @@ pub const LWFFrame = struct {
|
|||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *LWFFrame, allocator: std.mem.Allocator) void {
|
||||
pub fn deinit(self: *const LWFFrame, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.payload);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,21 @@ pub const pathfinding = @import("qvl/pathfinding.zig");
|
|||
pub const gossip = @import("qvl/gossip.zig");
|
||||
pub const inference = @import("qvl/inference.zig");
|
||||
pub const pop = @import("qvl/pop_integration.zig");
|
||||
pub const storage = @import("qvl/storage.zig");
|
||||
pub const integration = @import("qvl/integration.zig");
|
||||
pub const gql = @import("qvl/gql.zig");
|
||||
|
||||
pub const RiskEdge = types.RiskEdge;
|
||||
pub const NodeId = types.NodeId;
|
||||
pub const AnomalyScore = types.AnomalyScore;
|
||||
pub const PersistentGraph = storage.PersistentGraph;
|
||||
pub const HybridGraph = integration.HybridGraph;
|
||||
pub const GraphTransaction = integration.GraphTransaction;
|
||||
|
||||
// GQL exports
|
||||
pub const GQLQuery = gql.Query;
|
||||
pub const GQLStatement = gql.Statement;
|
||||
pub const parseGQL = gql.parse;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
//! GQL (Graph Query Language) for Libertaria QVL
|
||||
//!
|
||||
//! ISO/IEC 39075:2024 compliant implementation
|
||||
//! Entry point: parse(query_string) -> AST
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
pub const ast = @import("gql/ast.zig");
|
||||
pub const lexer = @import("gql/lexer.zig");
|
||||
pub const parser = @import("gql/parser.zig");
|
||||
pub const codegen = @import("gql/codegen.zig");
|
||||
|
||||
/// Parse GQL query string into AST
|
||||
pub fn parse(allocator: std.mem.Allocator, query: []const u8) !ast.Query {
|
||||
var lex = lexer.Lexer.init(query, allocator);
|
||||
const tokens = try lex.tokenize();
|
||||
defer allocator.free(tokens);
|
||||
|
||||
var par = parser.Parser.init(tokens, allocator);
|
||||
return try par.parse();
|
||||
}
|
||||
|
||||
/// Transpile GQL to Zig code (programmatic API)
|
||||
///
|
||||
/// Example:
|
||||
/// GQL: MATCH (n:Identity)-[t:TRUST]->(m) WHERE n.did = 'alice' RETURN m
|
||||
/// Zig: try graph.findTrustPath(alice, trust_filter)
|
||||
pub fn transpileToZig(allocator: std.mem.Allocator, query: ast.Query) ![]const u8 {
|
||||
// TODO: Implement code generation
|
||||
_ = allocator;
|
||||
_ = query;
|
||||
return "// TODO: Transpile GQL to Zig";
|
||||
}
|
||||
|
||||
// Re-export commonly used types
|
||||
pub const Query = ast.Query;
|
||||
pub const Statement = ast.Statement;
|
||||
pub const MatchStatement = ast.MatchStatement;
|
||||
pub const CreateStatement = ast.CreateStatement;
|
||||
pub const ReturnStatement = ast.ReturnStatement;
|
||||
pub const GraphPattern = ast.GraphPattern;
|
||||
pub const NodePattern = ast.NodePattern;
|
||||
pub const EdgePattern = ast.EdgePattern;
|
||||
|
||||
// Re-export code generator
|
||||
pub const generateZig = codegen.generate;
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
//! GQL (Graph Query Language) Parser
|
||||
//!
|
||||
//! ISO/IEC 39075:2024 compliant parser for Libertaria QVL.
|
||||
//! Transpiles GQL queries to Zig programmatic API calls.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// ============================================================================
|
||||
// AST TYPES
|
||||
// ============================================================================
|
||||
|
||||
/// Root node of a GQL query
|
||||
pub const Query = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
statements: []Statement,
|
||||
|
||||
pub fn deinit(self: *Query) void {
|
||||
for (self.statements) |*stmt| {
|
||||
stmt.deinit();
|
||||
}
|
||||
self.allocator.free(self.statements);
|
||||
}
|
||||
};
|
||||
|
||||
/// Statement types (GQL is statement-based)
|
||||
pub const Statement = union(enum) {
|
||||
match: MatchStatement,
|
||||
create: CreateStatement,
|
||||
delete: DeleteStatement,
|
||||
return_stmt: ReturnStatement,
|
||||
|
||||
pub fn deinit(self: *Statement) void {
|
||||
switch (self.*) {
|
||||
inline else => |*s| s.deinit(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// MATCH statement: pattern matching for graph traversal
|
||||
pub const MatchStatement = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
pattern: GraphPattern,
|
||||
where: ?Expression,
|
||||
|
||||
pub fn deinit(self: *MatchStatement) void {
|
||||
self.pattern.deinit();
|
||||
if (self.where) |*w| w.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
/// CREATE statement: insert nodes/edges
|
||||
pub const CreateStatement = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
pattern: GraphPattern,
|
||||
|
||||
pub fn deinit(self: *CreateStatement) void {
|
||||
self.pattern.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
/// DELETE statement: remove nodes/edges
|
||||
pub const DeleteStatement = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
targets: []Identifier,
|
||||
|
||||
pub fn deinit(self: *DeleteStatement) void {
|
||||
for (self.targets) |*t| t.deinit();
|
||||
self.allocator.free(self.targets);
|
||||
}
|
||||
};
|
||||
|
||||
/// RETURN statement: projection of results
|
||||
pub const ReturnStatement = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
items: []ReturnItem,
|
||||
|
||||
pub fn deinit(self: *ReturnStatement) void {
|
||||
for (self.items) |*item| item.deinit();
|
||||
self.allocator.free(self.items);
|
||||
}
|
||||
};
|
||||
|
||||
/// Graph pattern: sequence of path patterns
|
||||
pub const GraphPattern = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
paths: []PathPattern,
|
||||
|
||||
pub fn deinit(self: *GraphPattern) void {
|
||||
for (self.paths) |*p| p.deinit();
|
||||
self.allocator.free(self.paths);
|
||||
}
|
||||
};
|
||||
|
||||
/// Path pattern: node -edge-> node -edge-> ...
|
||||
pub const PathPattern = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
elements: []PathElement, // Alternating Node and Edge
|
||||
|
||||
pub fn deinit(self: *PathPattern) void {
|
||||
for (self.elements) |*e| e.deinit();
|
||||
self.allocator.free(self.elements);
|
||||
}
|
||||
};
|
||||
|
||||
/// Element in a path (node or edge)
|
||||
pub const PathElement = union(enum) {
|
||||
node: NodePattern,
|
||||
edge: EdgePattern,
|
||||
|
||||
pub fn deinit(self: *PathElement) void {
|
||||
switch (self.*) {
|
||||
inline else => |*e| e.deinit(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Node pattern: (n:Label {props})
|
||||
pub const NodePattern = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
variable: ?Identifier,
|
||||
labels: []Identifier,
|
||||
properties: ?PropertyMap,
|
||||
|
||||
pub fn deinit(self: *NodePattern) void {
|
||||
if (self.variable) |*v| v.deinit();
|
||||
for (self.labels) |*l| l.deinit();
|
||||
self.allocator.free(self.labels);
|
||||
if (self.properties) |*p| p.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
/// Edge pattern: -[r:TYPE {props}]-> or <-[...]-
|
||||
pub const EdgePattern = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
direction: EdgeDirection,
|
||||
variable: ?Identifier,
|
||||
types: []Identifier,
|
||||
properties: ?PropertyMap,
|
||||
quantifier: ?Quantifier, // *1..3 for variable length
|
||||
|
||||
pub fn deinit(self: *EdgePattern) void {
|
||||
if (self.variable) |*v| v.deinit();
|
||||
for (self.types) |*t| t.deinit();
|
||||
self.allocator.free(self.types);
|
||||
if (self.properties) |*p| p.deinit();
|
||||
if (self.quantifier) |*q| q.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
pub const EdgeDirection = enum {
|
||||
outgoing, // -
|
||||
incoming, // <-
|
||||
any, // -
|
||||
};
|
||||
|
||||
/// Quantifier for variable-length paths: *min..max
|
||||
pub const Quantifier = struct {
|
||||
min: ?u32,
|
||||
max: ?u32, // null = unlimited
|
||||
|
||||
pub fn deinit(self: *Quantifier) void {
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
/// Property map: {key: value, ...}
|
||||
pub const PropertyMap = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
entries: []PropertyEntry,
|
||||
|
||||
pub fn deinit(self: *PropertyMap) void {
|
||||
for (self.entries) |*e| e.deinit();
|
||||
self.allocator.free(self.entries);
|
||||
}
|
||||
};
|
||||
|
||||
pub const PropertyEntry = struct {
|
||||
key: Identifier,
|
||||
value: Expression,
|
||||
|
||||
pub fn deinit(self: *PropertyEntry) void {
|
||||
self.key.deinit();
|
||||
self.value.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
/// Return item: expression [AS alias]
|
||||
pub const ReturnItem = struct {
|
||||
expression: Expression,
|
||||
alias: ?Identifier,
|
||||
|
||||
pub fn deinit(self: *ReturnItem) void {
|
||||
self.expression.deinit();
|
||||
if (self.alias) |*a| a.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// EXPRESSIONS
|
||||
// ============================================================================
|
||||
|
||||
pub const Expression = union(enum) {
|
||||
literal: Literal,
|
||||
identifier: Identifier,
|
||||
property_access: PropertyAccess,
|
||||
binary_op: BinaryOp,
|
||||
comparison: Comparison,
|
||||
function_call: FunctionCall,
|
||||
list: ListExpression,
|
||||
|
||||
pub fn deinit(self: *Expression) void {
|
||||
switch (self.*) {
|
||||
inline else => |*e| e.deinit(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const Literal = union(enum) {
|
||||
string: []const u8,
|
||||
integer: i64,
|
||||
float: f64,
|
||||
boolean: bool,
|
||||
null: void,
|
||||
|
||||
pub fn deinit(self: *Literal) void {
|
||||
// Strings are slices into source - no cleanup needed
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
/// Identifier (variable, label, property name)
|
||||
pub const Identifier = struct {
|
||||
name: []const u8,
|
||||
|
||||
pub fn deinit(self: *Identifier) void {
|
||||
// No allocator needed - name is a slice into source
|
||||
_ = self;
|
||||
}
|
||||
};
|
||||
|
||||
/// Property access: node.property or edge.property
|
||||
pub const PropertyAccess = struct {
|
||||
object: Identifier,
|
||||
property: Identifier,
|
||||
|
||||
pub fn deinit(self: *PropertyAccess) void {
|
||||
self.object.deinit();
|
||||
self.property.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
/// Binary operation: a + b, a - b, etc.
|
||||
pub const BinaryOp = struct {
|
||||
left: *Expression,
|
||||
op: BinaryOperator,
|
||||
right: *Expression,
|
||||
|
||||
pub fn deinit(self: *BinaryOp) void {
|
||||
self.left.deinit();
|
||||
// Note: Can't free self.left/right without allocator
|
||||
// Memory managed by arena or leaked for now
|
||||
}
|
||||
};
|
||||
|
||||
pub const BinaryOperator = enum {
|
||||
add, sub, mul, div, mod,
|
||||
and_op, or_op,
|
||||
};
|
||||
|
||||
/// Comparison: a = b, a < b, etc.
|
||||
pub const Comparison = struct {
|
||||
left: *Expression,
|
||||
op: ComparisonOperator,
|
||||
right: *Expression,
|
||||
|
||||
pub fn deinit(self: *Comparison) void {
|
||||
self.left.deinit();
|
||||
self.right.deinit();
|
||||
// Note: Can't free self.left/right without allocator
|
||||
}
|
||||
};
|
||||
|
||||
pub const ComparisonOperator = enum {
|
||||
eq, // =
|
||||
neq, // <>
|
||||
lt, // <
|
||||
lte, // <=
|
||||
gt, // >
|
||||
gte, // >=
|
||||
};
|
||||
|
||||
/// Function call: function(arg1, arg2, ...)
|
||||
pub const FunctionCall = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
name: Identifier,
|
||||
args: []Expression,
|
||||
|
||||
pub fn deinit(self: *FunctionCall) void {
|
||||
self.name.deinit();
|
||||
for (self.args) |*a| a.deinit();
|
||||
self.allocator.free(self.args);
|
||||
}
|
||||
};
|
||||
|
||||
/// List literal: [1, 2, 3]
|
||||
pub const ListExpression = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
elements: []Expression,
|
||||
|
||||
pub fn deinit(self: *ListExpression) void {
|
||||
for (self.elements) |*e| e.deinit();
|
||||
self.allocator.free(self.elements);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
//! GQL to Zig Code Generator
|
||||
//!
|
||||
//! Transpiles GQL AST to Zig programmatic API calls.
|
||||
//! Turns declarative graph queries into imperative Zig code.
|
||||
|
||||
const std = @import("std");
|
||||
const ast = @import("ast.zig");
|
||||
|
||||
const Query = ast.Query;
|
||||
const Statement = ast.Statement;
|
||||
const MatchStatement = ast.MatchStatement;
|
||||
const CreateStatement = ast.CreateStatement;
|
||||
const GraphPattern = ast.GraphPattern;
|
||||
const PathPattern = ast.PathPattern;
|
||||
const NodePattern = ast.NodePattern;
|
||||
const EdgePattern = ast.EdgePattern;
|
||||
const Expression = ast.Expression;
|
||||
|
||||
/// Code generation context
|
||||
pub const CodeGenContext = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
indent_level: usize = 0,
|
||||
output: std.ArrayList(u8),
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Self {
|
||||
return Self{
|
||||
.allocator = allocator,
|
||||
.indent_level = 0,
|
||||
.output = std.ArrayList(u8){},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.output.deinit(self.allocator);
|
||||
}
|
||||
|
||||
pub fn getCode(self: *Self) ![]const u8 {
|
||||
return self.output.toOwnedSlice(self.allocator);
|
||||
}
|
||||
|
||||
fn write(self: *Self, text: []const u8) !void {
|
||||
try self.output.appendSlice(self.allocator, text);
|
||||
}
|
||||
|
||||
fn writeln(self: *Self, text: []const u8) !void {
|
||||
try self.writeIndent();
|
||||
try self.write(text);
|
||||
try self.write("\n");
|
||||
}
|
||||
|
||||
fn writeIndent(self: *Self) !void {
|
||||
for (0..self.indent_level) |_| {
|
||||
try self.write(" ");
|
||||
}
|
||||
}
|
||||
|
||||
fn indent(self: *Self) void {
|
||||
self.indent_level += 1;
|
||||
}
|
||||
|
||||
fn dedent(self: *Self) void {
|
||||
if (self.indent_level > 0) {
|
||||
self.indent_level -= 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Generate Zig code from GQL query
|
||||
pub fn generate(allocator: std.mem.Allocator, query: Query) ![]const u8 {
|
||||
var ctx = CodeGenContext.init(allocator);
|
||||
errdefer ctx.deinit();
|
||||
|
||||
// Header
|
||||
try ctx.writeln("// Auto-generated from GQL query");
|
||||
try ctx.writeln("// Libertaria QVL Programmatic API");
|
||||
try ctx.writeln("");
|
||||
try ctx.writeln("const std = @import(\"std\");");
|
||||
try ctx.writeln("const qvl = @import(\"qvl\");");
|
||||
try ctx.writeln("");
|
||||
try ctx.writeln("pub fn execute(graph: *qvl.HybridGraph) !void {");
|
||||
ctx.indent();
|
||||
|
||||
// Generate code for each statement
|
||||
for (query.statements) |stmt| {
|
||||
try generateStatement(&ctx, stmt);
|
||||
}
|
||||
|
||||
ctx.dedent();
|
||||
try ctx.writeln("}");
|
||||
|
||||
return ctx.getCode();
|
||||
}
|
||||
|
||||
fn generateStatement(ctx: *CodeGenContext, stmt: Statement) !void {
|
||||
switch (stmt) {
|
||||
.match => |m| try generateMatch(ctx, m),
|
||||
.create => |c| try generateCreate(ctx, c),
|
||||
.delete => |d| try generateDelete(ctx, d),
|
||||
.return_stmt => |r| try generateReturn(ctx, r),
|
||||
}
|
||||
}
|
||||
|
||||
fn generateMatch(ctx: *CodeGenContext, match: MatchStatement) !void {
|
||||
try ctx.writeln("");
|
||||
try ctx.writeln("// MATCH statement");
|
||||
|
||||
// Generate path traversal for each pattern
|
||||
for (match.pattern.paths) |path| {
|
||||
try generatePathTraversal(ctx, path);
|
||||
}
|
||||
|
||||
// Generate WHERE clause if present
|
||||
if (match.where) |where| {
|
||||
try ctx.write(" // WHERE ");
|
||||
try generateExpression(ctx, where);
|
||||
try ctx.write("\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn generatePathTraversal(ctx: *CodeGenContext, path: PathPattern) !void {
|
||||
// Path pattern: (a)-[r]->(b)-[s]->(c)
|
||||
// Generate: traverse from start node following edges
|
||||
|
||||
if (path.elements.len == 0) return;
|
||||
|
||||
// Get start node
|
||||
const start_node = path.elements[0].node;
|
||||
const start_var = start_node.variable orelse ast.Identifier{ .name = "_" };
|
||||
|
||||
try ctx.write(" // Traverse from ");
|
||||
try ctx.write(start_var.name);
|
||||
try ctx.write("\n");
|
||||
|
||||
// For simple 1-hop: getOutgoing and filter
|
||||
if (path.elements.len == 3) {
|
||||
// (a)-[r]->(b)
|
||||
const edge = path.elements[1].edge;
|
||||
const end_node = path.elements[2].node;
|
||||
|
||||
const edge_var = edge.variable orelse ast.Identifier{ .name = "edge" };
|
||||
const end_var = end_node.variable orelse ast.Identifier{ .name = "target" };
|
||||
|
||||
try ctx.write(" var ");
|
||||
try ctx.write(edge_var.name);
|
||||
try ctx.write(" = try graph.getOutgoing(");
|
||||
try ctx.write(start_var.name);
|
||||
try ctx.write(");\n");
|
||||
|
||||
// Filter by edge type if specified
|
||||
if (edge.types.len > 0) {
|
||||
try ctx.write(" // Filter by type: ");
|
||||
for (edge.types) |t| {
|
||||
try ctx.write(t.name);
|
||||
try ctx.write(" ");
|
||||
}
|
||||
try ctx.write("\n");
|
||||
}
|
||||
|
||||
try ctx.write(" var ");
|
||||
try ctx.write(end_var.name);
|
||||
try ctx.write(" = ");
|
||||
try ctx.write(edge_var.name);
|
||||
try ctx.write(".to;\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn generateCreate(ctx: *CodeGenContext, create: CreateStatement) !void {
|
||||
try ctx.writeln("");
|
||||
try ctx.writeln("// CREATE statement");
|
||||
|
||||
for (create.pattern.paths) |path| {
|
||||
// Create nodes and edges
|
||||
for (path.elements) |elem| {
|
||||
switch (elem) {
|
||||
.node => |n| {
|
||||
if (n.variable) |v| {
|
||||
try ctx.write(" const ");
|
||||
try ctx.write(v.name);
|
||||
try ctx.write(" = try graph.addNode(.{ .id = \"");
|
||||
try ctx.write(v.name);
|
||||
try ctx.write("\" });\n");
|
||||
}
|
||||
},
|
||||
.edge => |e| {
|
||||
if (e.variable) |v| {
|
||||
try ctx.write(" try graph.addEdge(");
|
||||
try ctx.write(v.name);
|
||||
try ctx.write(");\n");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generateDelete(ctx: *CodeGenContext, delete: ast.DeleteStatement) !void {
|
||||
try ctx.writeln("");
|
||||
try ctx.writeln("// DELETE statement");
|
||||
|
||||
for (delete.targets) |target| {
|
||||
try ctx.write(" try graph.removeNode(");
|
||||
try ctx.write(target.name);
|
||||
try ctx.write(");\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn generateReturn(ctx: *CodeGenContext, ret: ast.ReturnStatement) !void {
|
||||
try ctx.writeln("");
|
||||
try ctx.writeln("// RETURN statement");
|
||||
try ctx.writeln(" var results = std.ArrayList(Result).init(allocator);");
|
||||
try ctx.writeln(" defer results.deinit();");
|
||||
|
||||
for (ret.items) |item| {
|
||||
try ctx.write(" try results.append(");
|
||||
try generateExpression(ctx, item.expression);
|
||||
try ctx.write(");\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn generateExpression(ctx: *CodeGenContext, expr: Expression) !void {
|
||||
switch (expr) {
|
||||
.identifier => |i| try ctx.write(i.name),
|
||||
.literal => |l| try generateLiteral(ctx, l),
|
||||
.property_access => |p| {
|
||||
try ctx.write(p.object.name);
|
||||
try ctx.write(".");
|
||||
try ctx.write(p.property.name);
|
||||
},
|
||||
.comparison => |c| {
|
||||
try generateExpression(ctx, c.left.*);
|
||||
try ctx.write(" ");
|
||||
try ctx.write(comparisonOpToString(c.op));
|
||||
try ctx.write(" ");
|
||||
try generateExpression(ctx, c.right.*);
|
||||
},
|
||||
.binary_op => |b| {
|
||||
try generateExpression(ctx, b.left.*);
|
||||
try ctx.write(" ");
|
||||
try ctx.write(binaryOpToString(b.op));
|
||||
try ctx.write(" ");
|
||||
try generateExpression(ctx, b.right.*);
|
||||
},
|
||||
else => try ctx.write("/* complex expression */"),
|
||||
}
|
||||
}
|
||||
|
||||
fn generateLiteral(ctx: *CodeGenContext, literal: ast.Literal) !void {
|
||||
switch (literal) {
|
||||
.string => |s| {
|
||||
try ctx.write("\"");
|
||||
try ctx.write(s);
|
||||
try ctx.write("\"");
|
||||
},
|
||||
.integer => |i| {
|
||||
var buf: [32]u8 = undefined;
|
||||
const str = try std.fmt.bufPrint(&buf, "{d}", .{i});
|
||||
try ctx.write(str);
|
||||
},
|
||||
.float => |f| {
|
||||
var buf: [32]u8 = undefined;
|
||||
const str = try std.fmt.bufPrint(&buf, "{d}", .{f});
|
||||
try ctx.write(str);
|
||||
},
|
||||
.boolean => |b| try ctx.write(if (b) "true" else "false"),
|
||||
.null => try ctx.write("null"),
|
||||
}
|
||||
}
|
||||
|
||||
fn comparisonOpToString(op: ast.ComparisonOperator) []const u8 {
|
||||
return switch (op) {
|
||||
.eq => "==",
|
||||
.neq => "!=",
|
||||
.lt => "<",
|
||||
.lte => "<=",
|
||||
.gt => ">",
|
||||
.gte => ">=",
|
||||
};
|
||||
}
|
||||
|
||||
fn binaryOpToString(op: ast.BinaryOperator) []const u8 {
|
||||
return switch (op) {
|
||||
.add => "+",
|
||||
.sub => "-",
|
||||
.mul => "*",
|
||||
.div => "/",
|
||||
.mod => "%",
|
||||
.and_op => "and",
|
||||
.or_op => "or",
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TESTS
|
||||
// ============================================================================
|
||||
|
||||
test "Codegen: simple MATCH" {
|
||||
const allocator = std.testing.allocator;
|
||||
const gql = "MATCH (n:Identity) RETURN n";
|
||||
|
||||
var lex = @import("lexer.zig").Lexer.init(gql, allocator);
|
||||
const tokens = try lex.tokenize();
|
||||
defer allocator.free(tokens);
|
||||
|
||||
var parser = @import("parser.zig").Parser.init(tokens, allocator);
|
||||
var query = try parser.parse();
|
||||
defer query.deinit();
|
||||
|
||||
const code = try generate(allocator, query);
|
||||
defer allocator.free(code);
|
||||
|
||||
// Check that generated code contains expected patterns
|
||||
const code_str = code;
|
||||
try std.testing.expect(std.mem.indexOf(u8, code_str, "execute") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, code_str, "HybridGraph") != null);
|
||||
}
|
||||
|
|
@ -0,0 +1,417 @@
|
|||
//! GQL Lexer/Tokenizer
|
||||
//!
|
||||
//! Converts GQL query string into tokens for parser.
|
||||
//! ISO/IEC 39075:2024 lexical structure.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
pub const TokenType = enum {
|
||||
// Keywords
|
||||
match,
|
||||
create,
|
||||
delete,
|
||||
return_keyword,
|
||||
where,
|
||||
as_keyword,
|
||||
and_keyword,
|
||||
or_keyword,
|
||||
not_keyword,
|
||||
null_keyword,
|
||||
true_keyword,
|
||||
false_keyword,
|
||||
|
||||
// Punctuation
|
||||
left_paren, // (
|
||||
right_paren, // )
|
||||
left_bracket, // [
|
||||
right_bracket, // ]
|
||||
left_brace, // {
|
||||
right_brace, // }
|
||||
colon, // :
|
||||
comma, // ,
|
||||
dot, // .
|
||||
minus, // -
|
||||
arrow_right, // ->
|
||||
arrow_left, // <-
|
||||
star, // *
|
||||
slash, // /
|
||||
percent, // %
|
||||
plus, // +
|
||||
|
||||
// Comparison operators
|
||||
eq, // =
|
||||
neq, // <>
|
||||
lt, // <
|
||||
lte, // <=
|
||||
gt, // >
|
||||
gte, // >=
|
||||
|
||||
// Literals
|
||||
identifier,
|
||||
string_literal,
|
||||
integer_literal,
|
||||
float_literal,
|
||||
|
||||
// Special
|
||||
eof,
|
||||
invalid,
|
||||
};
|
||||
|
||||
pub const Token = struct {
|
||||
type: TokenType,
|
||||
text: []const u8, // Slice into original source
|
||||
line: u32,
|
||||
column: u32,
|
||||
};
|
||||
|
||||
pub const Lexer = struct {
|
||||
source: []const u8,
|
||||
pos: usize,
|
||||
line: u32,
|
||||
column: u32,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(source: []const u8, allocator: std.mem.Allocator) Self {
|
||||
return Self{
|
||||
.source = source,
|
||||
.pos = 0,
|
||||
.line = 1,
|
||||
.column = 1,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get next token
|
||||
pub fn nextToken(self: *Self) !Token {
|
||||
self.skipWhitespace();
|
||||
|
||||
if (self.pos >= self.source.len) {
|
||||
return self.makeToken(.eof, 0);
|
||||
}
|
||||
|
||||
const c = self.source[self.pos];
|
||||
|
||||
// Identifiers and keywords
|
||||
if (isAlpha(c) or c == '_') {
|
||||
return self.readIdentifier();
|
||||
}
|
||||
|
||||
// Numbers
|
||||
if (isDigit(c)) {
|
||||
return self.readNumber();
|
||||
}
|
||||
|
||||
// Strings
|
||||
if (c == '"' or c == '\'') {
|
||||
return self.readString();
|
||||
}
|
||||
|
||||
// Single-char tokens and operators
|
||||
switch (c) {
|
||||
'(' => { self.advance(); return self.makeToken(.left_paren, 1); },
|
||||
')' => { self.advance(); return self.makeToken(.right_paren, 1); },
|
||||
'[' => { self.advance(); return self.makeToken(.left_bracket, 1); },
|
||||
']' => { self.advance(); return self.makeToken(.right_bracket, 1); },
|
||||
'{' => { self.advance(); return self.makeToken(.left_brace, 1); },
|
||||
'}' => { self.advance(); return self.makeToken(.right_brace, 1); },
|
||||
':' => { self.advance(); return self.makeToken(.colon, 1); },
|
||||
',' => { self.advance(); return self.makeToken(.comma, 1); },
|
||||
'.' => { self.advance(); return self.makeToken(.dot, 1); },
|
||||
'+' => { self.advance(); return self.makeToken(.plus, 1); },
|
||||
'%' => { self.advance(); return self.makeToken(.percent, 1); },
|
||||
'*' => { self.advance(); return self.makeToken(.star, 1); },
|
||||
|
||||
'-' => {
|
||||
self.advance();
|
||||
if (self.peek() == '>') {
|
||||
self.advance();
|
||||
return self.makeToken(.arrow_right, 2);
|
||||
}
|
||||
return self.makeToken(.minus, 1);
|
||||
},
|
||||
|
||||
'<' => {
|
||||
self.advance();
|
||||
if (self.peek() == '-') {
|
||||
self.advance();
|
||||
return self.makeToken(.arrow_left, 2);
|
||||
} else if (self.peek() == '>') {
|
||||
self.advance();
|
||||
return self.makeToken(.neq, 2);
|
||||
} else if (self.peek() == '=') {
|
||||
self.advance();
|
||||
return self.makeToken(.lte, 2);
|
||||
}
|
||||
return self.makeToken(.lt, 1);
|
||||
},
|
||||
|
||||
'>' => {
|
||||
self.advance();
|
||||
if (self.peek() == '=') {
|
||||
self.advance();
|
||||
return self.makeToken(.gte, 2);
|
||||
}
|
||||
return self.makeToken(.gt, 1);
|
||||
},
|
||||
|
||||
'=' => { self.advance(); return self.makeToken(.eq, 1); },
|
||||
|
||||
else => {
|
||||
self.advance();
|
||||
return self.makeToken(.invalid, 1);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Read all tokens into array
|
||||
pub fn tokenize(self: *Self) ![]Token {
|
||||
var tokens: std.ArrayList(Token) = .{};
|
||||
errdefer tokens.deinit(self.allocator);
|
||||
|
||||
while (true) {
|
||||
const tok = try self.nextToken();
|
||||
try tokens.append(self.allocator, tok);
|
||||
if (tok.type == .eof) break;
|
||||
}
|
||||
|
||||
return tokens.toOwnedSlice(self.allocator);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Internal helpers
|
||||
// =========================================================================
|
||||
|
||||
fn advance(self: *Self) void {
|
||||
if (self.pos >= self.source.len) return;
|
||||
|
||||
if (self.source[self.pos] == '\n') {
|
||||
self.line += 1;
|
||||
self.column = 1;
|
||||
} else {
|
||||
self.column += 1;
|
||||
}
|
||||
self.pos += 1;
|
||||
}
|
||||
|
||||
fn peek(self: *Self) u8 {
|
||||
if (self.pos >= self.source.len) return 0;
|
||||
return self.source[self.pos];
|
||||
}
|
||||
|
||||
fn skipWhitespace(self: *Self) void {
|
||||
while (self.pos < self.source.len) {
|
||||
const c = self.source[self.pos];
|
||||
if (c == ' ' or c == '\t' or c == '\n' or c == '\r') {
|
||||
self.advance();
|
||||
} else if (c == '/' and self.pos + 1 < self.source.len and self.source[self.pos + 1] == '/') {
|
||||
// Single-line comment
|
||||
while (self.pos < self.source.len and self.source[self.pos] != '\n') {
|
||||
self.advance();
|
||||
}
|
||||
} else if (c == '/' and self.pos + 1 < self.source.len and self.source[self.pos + 1] == '*') {
|
||||
// Multi-line comment
|
||||
self.advance(); // /
|
||||
self.advance(); // *
|
||||
while (self.pos + 1 < self.source.len) {
|
||||
if (self.source[self.pos] == '*' and self.source[self.pos + 1] == '/') {
|
||||
self.advance(); // *
|
||||
self.advance(); // /
|
||||
break;
|
||||
}
|
||||
self.advance();
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn readIdentifier(self: *Self) Token {
|
||||
const start = self.pos;
|
||||
const start_line = self.line;
|
||||
const start_col = self.column;
|
||||
|
||||
while (self.pos < self.source.len) {
|
||||
const c = self.source[self.pos];
|
||||
if (isAlphaNum(c) or c == '_') {
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const text = self.source[start..self.pos];
|
||||
const tok_type = keywordFromString(text);
|
||||
|
||||
return Token{
|
||||
.type = tok_type,
|
||||
.text = text,
|
||||
.line = start_line,
|
||||
.column = start_col,
|
||||
};
|
||||
}
|
||||
|
||||
fn readNumber(self: *Self) !Token {
|
||||
const start = self.pos;
|
||||
const start_line = self.line;
|
||||
const start_col = self.column;
|
||||
var is_float = false;
|
||||
|
||||
while (self.pos < self.source.len) {
|
||||
const c = self.source[self.pos];
|
||||
if (isDigit(c)) {
|
||||
self.advance();
|
||||
} else if (c == '.' and !is_float) {
|
||||
// Check for range operator (e.g., 1..3)
|
||||
if (self.pos + 1 < self.source.len and self.source[self.pos + 1] == '.') {
|
||||
break; // Stop before range operator
|
||||
}
|
||||
is_float = true;
|
||||
self.advance();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const text = self.source[start..self.pos];
|
||||
const tok_type: TokenType = if (is_float) .float_literal else .integer_literal;
|
||||
|
||||
return Token{
|
||||
.type = tok_type,
|
||||
.text = text,
|
||||
.line = start_line,
|
||||
.column = start_col,
|
||||
};
|
||||
}
|
||||
|
||||
fn readString(self: *Self) !Token {
|
||||
const start = self.pos;
|
||||
const start_line = self.line;
|
||||
const start_col = self.column;
|
||||
const quote = self.source[self.pos];
|
||||
self.advance(); // opening quote
|
||||
|
||||
while (self.pos < self.source.len) {
|
||||
const c = self.source[self.pos];
|
||||
if (c == quote) {
|
||||
self.advance(); // closing quote
|
||||
break;
|
||||
} else if (c == '\\' and self.pos + 1 < self.source.len) {
|
||||
self.advance(); // backslash
|
||||
self.advance(); // escaped char
|
||||
} else {
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
|
||||
const text = self.source[start..self.pos];
|
||||
return Token{
|
||||
.type = .string_literal,
|
||||
.text = text,
|
||||
.line = start_line,
|
||||
.column = start_col,
|
||||
};
|
||||
}
|
||||
|
||||
fn makeToken(self: *Self, tok_type: TokenType, len: usize) Token {
|
||||
const tok = Token{
|
||||
.type = tok_type,
|
||||
.text = self.source[self.pos - len .. self.pos],
|
||||
.line = self.line,
|
||||
.column = self.column - @as(u32, @intCast(len)),
|
||||
};
|
||||
return tok;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Helper functions
|
||||
// ============================================================================
|
||||
|
||||
fn isAlpha(c: u8) bool {
|
||||
return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z');
|
||||
}
|
||||
|
||||
fn isDigit(c: u8) bool {
|
||||
return c >= '0' and c <= '9';
|
||||
}
|
||||
|
||||
fn isAlphaNum(c: u8) bool {
|
||||
return isAlpha(c) or isDigit(c);
|
||||
}
|
||||
|
||||
fn keywordFromString(text: []const u8) TokenType {
|
||||
// Zig 0.15.2 compatible: use switch instead of ComptimeStringMap
|
||||
if (std.mem.eql(u8, text, "MATCH") or std.mem.eql(u8, text, "match")) return .match;
|
||||
if (std.mem.eql(u8, text, "CREATE") or std.mem.eql(u8, text, "create")) return .create;
|
||||
if (std.mem.eql(u8, text, "DELETE") or std.mem.eql(u8, text, "delete")) return .delete;
|
||||
if (std.mem.eql(u8, text, "RETURN") or std.mem.eql(u8, text, "return")) return .return_keyword;
|
||||
if (std.mem.eql(u8, text, "WHERE") or std.mem.eql(u8, text, "where")) return .where;
|
||||
if (std.mem.eql(u8, text, "AS") or std.mem.eql(u8, text, "as")) return .as_keyword;
|
||||
if (std.mem.eql(u8, text, "AND") or std.mem.eql(u8, text, "and")) return .and_keyword;
|
||||
if (std.mem.eql(u8, text, "OR") or std.mem.eql(u8, text, "or")) return .or_keyword;
|
||||
if (std.mem.eql(u8, text, "NOT") or std.mem.eql(u8, text, "not")) return .not_keyword;
|
||||
if (std.mem.eql(u8, text, "NULL") or std.mem.eql(u8, text, "null")) return .null_keyword;
|
||||
if (std.mem.eql(u8, text, "TRUE") or std.mem.eql(u8, text, "true")) return .true_keyword;
|
||||
if (std.mem.eql(u8, text, "FALSE") or std.mem.eql(u8, text, "false")) return .false_keyword;
|
||||
return .identifier;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TESTS
|
||||
// ============================================================================
|
||||
|
||||
test "Lexer: simple keywords" {
|
||||
const allocator = std.testing.allocator;
|
||||
const source = "MATCH (n) RETURN n";
|
||||
|
||||
var lex = Lexer.init(source, allocator);
|
||||
const tokens = try lex.tokenize();
|
||||
defer allocator.free(tokens);
|
||||
|
||||
try std.testing.expectEqual(TokenType.match, tokens[0].type);
|
||||
try std.testing.expectEqual(TokenType.left_paren, tokens[1].type);
|
||||
try std.testing.expectEqual(TokenType.identifier, tokens[2].type);
|
||||
try std.testing.expectEqual(TokenType.right_paren, tokens[3].type);
|
||||
try std.testing.expectEqual(TokenType.return_keyword, tokens[4].type);
|
||||
try std.testing.expectEqual(TokenType.identifier, tokens[5].type);
|
||||
try std.testing.expectEqual(TokenType.eof, tokens[6].type);
|
||||
}
|
||||
|
||||
test "Lexer: arrow operators" {
|
||||
const allocator = std.testing.allocator;
|
||||
const source = "-> <-";
|
||||
|
||||
var lexer = Lexer.init(source, allocator);
|
||||
const tokens = try lexer.tokenize();
|
||||
defer allocator.free(tokens);
|
||||
|
||||
try std.testing.expectEqual(TokenType.arrow_right, tokens[0].type);
|
||||
try std.testing.expectEqual(TokenType.arrow_left, tokens[1].type);
|
||||
}
|
||||
|
||||
test "Lexer: string literal" {
|
||||
const allocator = std.testing.allocator;
|
||||
const source = "\"hello world\"";
|
||||
|
||||
var lexer = Lexer.init(source, allocator);
|
||||
const tokens = try lexer.tokenize();
|
||||
defer allocator.free(tokens);
|
||||
|
||||
try std.testing.expectEqual(TokenType.string_literal, tokens[0].type);
|
||||
try std.testing.expectEqualStrings("\"hello world\"", tokens[0].text);
|
||||
}
|
||||
|
||||
test "Lexer: numbers" {
|
||||
const allocator = std.testing.allocator;
|
||||
const source = "42 3.14";
|
||||
|
||||
var lexer = Lexer.init(source, allocator);
|
||||
const tokens = try lexer.tokenize();
|
||||
defer allocator.free(tokens);
|
||||
|
||||
try std.testing.expectEqual(TokenType.integer_literal, tokens[0].type);
|
||||
try std.testing.expectEqual(TokenType.float_literal, tokens[1].type);
|
||||
}
|
||||
|
|
@ -0,0 +1,562 @@
|
|||
//! GQL Parser (Recursive Descent)
|
||||
//!
|
||||
//! Parses GQL tokens into AST according to ISO/IEC 39075:2024.
|
||||
//! Entry point: Parser.parse() -> Query AST
|
||||
|
||||
const std = @import("std");
|
||||
const lexer = @import("lexer.zig");
|
||||
const ast = @import("ast.zig");
|
||||
|
||||
const Token = lexer.Token;
|
||||
const TokenType = lexer.TokenType;
|
||||
|
||||
pub const Parser = struct {
|
||||
tokens: []const Token,
|
||||
pos: usize,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(tokens: []const Token, allocator: std.mem.Allocator) Self {
|
||||
return Self{
|
||||
.tokens = tokens,
|
||||
.pos = 0,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
/// Parse complete query
|
||||
pub fn parse(self: *Self) !ast.Query {
|
||||
var statements = std.ArrayList(ast.Statement){};
|
||||
errdefer {
|
||||
for (statements.items) |*s| s.deinit();
|
||||
statements.deinit(self.allocator);
|
||||
}
|
||||
|
||||
while (!self.isAtEnd()) {
|
||||
const stmt = try self.parseStatement();
|
||||
try statements.append(self.allocator, stmt);
|
||||
}
|
||||
|
||||
return ast.Query{
|
||||
.allocator = self.allocator,
|
||||
.statements = try statements.toOwnedSlice(self.allocator),
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Statement parsing
|
||||
// =========================================================================
|
||||
|
||||
fn parseStatement(self: *Self) !ast.Statement {
|
||||
if (self.match(.match)) {
|
||||
return ast.Statement{ .match = try self.parseMatchStatement() };
|
||||
}
|
||||
if (self.match(.create)) {
|
||||
return ast.Statement{ .create = try self.parseCreateStatement() };
|
||||
}
|
||||
if (self.match(.return_keyword)) {
|
||||
return ast.Statement{ .return_stmt = try self.parseReturnStatement() };
|
||||
}
|
||||
if (self.match(.delete)) {
|
||||
return ast.Statement{ .delete = try self.parseDeleteStatement() };
|
||||
}
|
||||
|
||||
return error.UnexpectedToken;
|
||||
}
|
||||
|
||||
fn parseMatchStatement(self: *Self) !ast.MatchStatement {
|
||||
var pattern = try self.parseGraphPattern();
|
||||
errdefer pattern.deinit();
|
||||
|
||||
var where: ?ast.Expression = null;
|
||||
if (self.match(.where)) {
|
||||
where = try self.parseExpression();
|
||||
}
|
||||
|
||||
return ast.MatchStatement{
|
||||
.allocator = self.allocator,
|
||||
.pattern = pattern,
|
||||
.where = where,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseCreateStatement(self: *Self) !ast.CreateStatement {
|
||||
const pattern = try self.parseGraphPattern();
|
||||
|
||||
return ast.CreateStatement{
|
||||
.allocator = self.allocator,
|
||||
.pattern = pattern,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseDeleteStatement(self: *Self) !ast.DeleteStatement {
|
||||
// Simple: DELETE identifier [, identifier]*
|
||||
var targets = std.ArrayList(ast.Identifier){};
|
||||
errdefer {
|
||||
for (targets.items) |*t| t.deinit();
|
||||
targets.deinit(self.allocator);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const ident = try self.parseIdentifier();
|
||||
try targets.append(self.allocator, ident);
|
||||
|
||||
if (!self.match(.comma)) break;
|
||||
}
|
||||
|
||||
return ast.DeleteStatement{
|
||||
.allocator = self.allocator,
|
||||
.targets = try targets.toOwnedSlice(self.allocator),
|
||||
};
|
||||
}
|
||||
|
||||
fn parseReturnStatement(self: *Self) !ast.ReturnStatement {
|
||||
var items = std.ArrayList(ast.ReturnItem){};
|
||||
errdefer {
|
||||
for (items.items) |*i| i.deinit();
|
||||
items.deinit(self.allocator);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const expr = try self.parseExpression();
|
||||
|
||||
var alias: ?ast.Identifier = null;
|
||||
if (self.match(.as_keyword)) {
|
||||
alias = try self.parseIdentifier();
|
||||
}
|
||||
|
||||
try items.append(self.allocator, ast.ReturnItem{
|
||||
.expression = expr,
|
||||
.alias = alias,
|
||||
});
|
||||
|
||||
if (!self.match(.comma)) break;
|
||||
}
|
||||
|
||||
return ast.ReturnStatement{
|
||||
.allocator = self.allocator,
|
||||
.items = try items.toOwnedSlice(self.allocator),
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Pattern parsing
|
||||
// =========================================================================
|
||||
|
||||
fn parseGraphPattern(self: *Self) !ast.GraphPattern {
|
||||
var paths = std.ArrayList(ast.PathPattern){};
|
||||
errdefer {
|
||||
for (paths.items) |*p| p.deinit();
|
||||
paths.deinit(self.allocator);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const path = try self.parsePathPattern();
|
||||
try paths.append(self.allocator, path);
|
||||
|
||||
if (!self.match(.comma)) break;
|
||||
}
|
||||
|
||||
return ast.GraphPattern{
|
||||
.allocator = self.allocator,
|
||||
.paths = try paths.toOwnedSlice(self.allocator),
|
||||
};
|
||||
}
|
||||
|
||||
fn parsePathPattern(self: *Self) !ast.PathPattern {
|
||||
var elements = std.ArrayList(ast.PathElement){};
|
||||
errdefer {
|
||||
for (elements.items) |*e| e.deinit();
|
||||
elements.deinit(self.allocator);
|
||||
}
|
||||
|
||||
// Must start with a node
|
||||
const node = try self.parseNodePattern();
|
||||
try elements.append(self.allocator, ast.PathElement{ .node = node });
|
||||
|
||||
// Optional: edge - node - edge - node ...
|
||||
while (self.check(.minus) or self.check(.arrow_left)) {
|
||||
const edge = try self.parseEdgePattern();
|
||||
try elements.append(self.allocator, ast.PathElement{ .edge = edge });
|
||||
|
||||
const next_node = try self.parseNodePattern();
|
||||
try elements.append(self.allocator, ast.PathElement{ .node = next_node });
|
||||
}
|
||||
|
||||
return ast.PathPattern{
|
||||
.allocator = self.allocator,
|
||||
.elements = try elements.toOwnedSlice(self.allocator),
|
||||
};
|
||||
}
|
||||
|
||||
fn parseNodePattern(self: *Self) !ast.NodePattern {
|
||||
_ = try self.consume(.left_paren, "Expected '('");
|
||||
|
||||
// Optional variable: (n) or (:Label)
|
||||
var variable: ?ast.Identifier = null;
|
||||
if (self.check(.identifier)) {
|
||||
variable = try self.parseIdentifier();
|
||||
}
|
||||
|
||||
// Optional labels: (:Label1:Label2)
|
||||
var labels = std.ArrayList(ast.Identifier){};
|
||||
errdefer {
|
||||
for (labels.items) |*l| l.deinit();
|
||||
labels.deinit(self.allocator);
|
||||
}
|
||||
|
||||
while (self.match(.colon)) {
|
||||
const label = try self.parseIdentifier();
|
||||
try labels.append(self.allocator, label);
|
||||
}
|
||||
|
||||
// Optional properties: ({key: value})
|
||||
var properties: ?ast.PropertyMap = null;
|
||||
if (self.check(.left_brace)) {
|
||||
properties = try self.parsePropertyMap();
|
||||
}
|
||||
|
||||
_ = try self.consume(.right_paren, "Expected ')'");
|
||||
|
||||
return ast.NodePattern{
|
||||
.allocator = self.allocator,
|
||||
.variable = variable,
|
||||
.labels = try labels.toOwnedSlice(self.allocator),
|
||||
.properties = properties,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseEdgePattern(self: *Self) !ast.EdgePattern {
|
||||
var direction: ast.EdgeDirection = .outgoing;
|
||||
|
||||
// Check for incoming: <-
|
||||
if (self.match(.arrow_left)) {
|
||||
direction = .incoming;
|
||||
} else if (self.match(.minus)) {
|
||||
direction = .outgoing;
|
||||
}
|
||||
|
||||
// Edge details in brackets: -[r:TYPE]-
|
||||
var variable: ?ast.Identifier = null;
|
||||
var types = std.ArrayList(ast.Identifier){};
|
||||
errdefer {
|
||||
for (types.items) |*t| t.deinit();
|
||||
types.deinit(self.allocator);
|
||||
}
|
||||
var properties: ?ast.PropertyMap = null;
|
||||
var quantifier: ?ast.Quantifier = null;
|
||||
|
||||
if (self.match(.left_bracket)) {
|
||||
// Variable: [r]
|
||||
if (self.check(.identifier)) {
|
||||
variable = try self.parseIdentifier();
|
||||
}
|
||||
|
||||
// Type: [:TRUST]
|
||||
while (self.match(.colon)) {
|
||||
const edge_type = try self.parseIdentifier();
|
||||
try types.append(self.allocator, edge_type);
|
||||
}
|
||||
|
||||
// Properties: [{level: 3}]
|
||||
if (self.check(.left_brace)) {
|
||||
properties = try self.parsePropertyMap();
|
||||
}
|
||||
|
||||
// Quantifier: [*1..3]
|
||||
if (self.match(.star)) {
|
||||
quantifier = try self.parseQuantifier();
|
||||
}
|
||||
|
||||
_ = try self.consume(.right_bracket, "Expected ']'");
|
||||
}
|
||||
|
||||
// Arrow end
|
||||
if (direction == .outgoing) {
|
||||
_ = try self.consume(.arrow_right, "Expected '->'");
|
||||
} else {
|
||||
// Incoming already consumed <-, now just need -
|
||||
_ = try self.consume(.minus, "Expected '-'");
|
||||
}
|
||||
|
||||
return ast.EdgePattern{
|
||||
.allocator = self.allocator,
|
||||
.direction = direction,
|
||||
.variable = variable,
|
||||
.types = try types.toOwnedSlice(self.allocator),
|
||||
.properties = properties,
|
||||
.quantifier = quantifier,
|
||||
};
|
||||
}
|
||||
|
||||
fn parseQuantifier(self: *Self) !ast.Quantifier {
|
||||
var min: ?u32 = null;
|
||||
var max: ?u32 = null;
|
||||
|
||||
if (self.check(.integer_literal)) {
|
||||
min = try self.parseInteger();
|
||||
}
|
||||
|
||||
if (self.match(.dot) and self.match(.dot)) {
|
||||
if (self.check(.integer_literal)) {
|
||||
max = try self.parseInteger();
|
||||
}
|
||||
}
|
||||
|
||||
return ast.Quantifier{
|
||||
.min = min,
|
||||
.max = max,
|
||||
};
|
||||
}
|
||||
|
||||
fn parsePropertyMap(self: *Self) !ast.PropertyMap {
|
||||
_ = try self.consume(.left_brace, "Expected '{'");
|
||||
|
||||
var entries = std.ArrayList(ast.PropertyEntry){};
|
||||
errdefer {
|
||||
for (entries.items) |*e| e.deinit();
|
||||
entries.deinit(self.allocator);
|
||||
}
|
||||
|
||||
while (!self.check(.right_brace) and !self.isAtEnd()) {
|
||||
const key = try self.parseIdentifier();
|
||||
_ = try self.consume(.colon, "Expected ':'");
|
||||
const value = try self.parseExpression();
|
||||
|
||||
try entries.append(self.allocator, ast.PropertyEntry{
|
||||
.key = key,
|
||||
.value = value,
|
||||
});
|
||||
|
||||
if (!self.match(.comma)) break;
|
||||
}
|
||||
|
||||
_ = try self.consume(.right_brace, "Expected '}'");
|
||||
|
||||
return ast.PropertyMap{
|
||||
.allocator = self.allocator,
|
||||
.entries = try entries.toOwnedSlice(self.allocator),
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Expression parsing
|
||||
// =========================================================================
|
||||
|
||||
fn parseExpression(self: *Self) !ast.Expression {
|
||||
return try self.parseOrExpression();
|
||||
}
|
||||
|
||||
fn parseOrExpression(self: *Self) !ast.Expression {
|
||||
var left = try self.parseAndExpression();
|
||||
|
||||
while (self.match(.or_keyword)) {
|
||||
const right = try self.parseAndExpression();
|
||||
|
||||
// Create binary op
|
||||
const left_ptr = try self.allocator.create(ast.Expression);
|
||||
left_ptr.* = left;
|
||||
|
||||
const right_ptr = try self.allocator.create(ast.Expression);
|
||||
right_ptr.* = right;
|
||||
|
||||
left = ast.Expression{
|
||||
.binary_op = ast.BinaryOp{
|
||||
.left = left_ptr,
|
||||
.op = .or_op,
|
||||
.right = right_ptr,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
fn parseAndExpression(self: *Self) !ast.Expression {
|
||||
var left = try self.parseComparison();
|
||||
|
||||
while (self.match(.and_keyword)) {
|
||||
const right = try self.parseComparison();
|
||||
|
||||
const left_ptr = try self.allocator.create(ast.Expression);
|
||||
left_ptr.* = left;
|
||||
|
||||
const right_ptr = try self.allocator.create(ast.Expression);
|
||||
right_ptr.* = right;
|
||||
|
||||
left = ast.Expression{
|
||||
.binary_op = ast.BinaryOp{
|
||||
.left = left_ptr,
|
||||
.op = .and_op,
|
||||
.right = right_ptr,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
fn parseComparison(self: *Self) !ast.Expression {
|
||||
const left = try self.parseAdditive();
|
||||
|
||||
const op: ?ast.ComparisonOperator = blk: {
|
||||
if (self.match(.eq)) break :blk .eq;
|
||||
if (self.match(.neq)) break :blk .neq;
|
||||
if (self.match(.lt)) break :blk .lt;
|
||||
if (self.match(.lte)) break :blk .lte;
|
||||
if (self.match(.gt)) break :blk .gt;
|
||||
if (self.match(.gte)) break :blk .gte;
|
||||
break :blk null;
|
||||
};
|
||||
|
||||
if (op) |comparison_op| {
|
||||
const right = try self.parseAdditive();
|
||||
|
||||
const left_ptr = try self.allocator.create(ast.Expression);
|
||||
left_ptr.* = left;
|
||||
|
||||
const right_ptr = try self.allocator.create(ast.Expression);
|
||||
right_ptr.* = right;
|
||||
|
||||
return ast.Expression{
|
||||
.comparison = ast.Comparison{
|
||||
.left = left_ptr,
|
||||
.op = comparison_op,
|
||||
.right = right_ptr,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
fn parseAdditive(self: *Self) !ast.Expression {
|
||||
// Simplified: just return primary for now
|
||||
return try self.parsePrimary();
|
||||
}
|
||||
|
||||
fn parsePrimary(self: *Self) !ast.Expression {
|
||||
if (self.match(.null_keyword)) {
|
||||
return ast.Expression{ .literal = ast.Literal{ .null = {} } };
|
||||
}
|
||||
if (self.match(.true_keyword)) {
|
||||
return ast.Expression{ .literal = ast.Literal{ .boolean = true } };
|
||||
}
|
||||
if (self.match(.false_keyword)) {
|
||||
return ast.Expression{ .literal = ast.Literal{ .boolean = false } };
|
||||
}
|
||||
if (self.match(.string_literal)) {
|
||||
return ast.Expression{ .literal = ast.Literal{ .string = self.previous().text } };
|
||||
}
|
||||
if (self.check(.integer_literal)) {
|
||||
const val = try self.parseInteger();
|
||||
return ast.Expression{ .literal = ast.Literal{ .integer = @intCast(val) } };
|
||||
}
|
||||
|
||||
// Property access or identifier
|
||||
if (self.check(.identifier)) {
|
||||
const ident = try self.parseIdentifier();
|
||||
|
||||
if (self.match(.dot)) {
|
||||
const property = try self.parseIdentifier();
|
||||
return ast.Expression{
|
||||
.property_access = ast.PropertyAccess{
|
||||
.object = ident,
|
||||
.property = property,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return ast.Expression{ .identifier = ident };
|
||||
}
|
||||
|
||||
return error.UnexpectedToken;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
|
||||
fn parseIdentifier(self: *Self) !ast.Identifier {
|
||||
const tok = try self.consume(.identifier, "Expected identifier");
|
||||
return ast.Identifier{ .name = tok.text };
|
||||
}
|
||||
|
||||
fn parseInteger(self: *Self) !u32 {
|
||||
const tok = try self.consume(.integer_literal, "Expected integer");
|
||||
return try std.fmt.parseInt(u32, tok.text, 10);
|
||||
}
|
||||
|
||||
fn match(self: *Self, tok_type: TokenType) bool {
|
||||
if (self.check(tok_type)) {
|
||||
_ = self.advance();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn check(self: *Self, tok_type: TokenType) bool {
|
||||
if (self.isAtEnd()) return false;
|
||||
return self.peek().type == tok_type;
|
||||
}
|
||||
|
||||
fn advance(self: *Self) Token {
|
||||
if (!self.isAtEnd()) self.pos += 1;
|
||||
return self.previous();
|
||||
}
|
||||
|
||||
fn isAtEnd(self: *Self) bool {
|
||||
return self.peek().type == .eof;
|
||||
}
|
||||
|
||||
fn peek(self: *Self) Token {
|
||||
return self.tokens[self.pos];
|
||||
}
|
||||
|
||||
fn previous(self: *Self) Token {
|
||||
return self.tokens[self.pos - 1];
|
||||
}
|
||||
|
||||
fn consume(self: *Self, tok_type: TokenType, message: []const u8) !Token {
|
||||
if (self.check(tok_type)) return self.advance();
|
||||
std.log.err("{s}, got {s}", .{ message, @tagName(self.peek().type) });
|
||||
return error.UnexpectedToken;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// TESTS
|
||||
// ============================================================================
|
||||
|
||||
test "Parser: simple MATCH" {
|
||||
const allocator = std.testing.allocator;
|
||||
const source = "MATCH (n:Identity) RETURN n";
|
||||
|
||||
var lex = lexer.Lexer.init(source, allocator);
|
||||
const tokens = try lex.tokenize();
|
||||
defer allocator.free(tokens);
|
||||
|
||||
var parser = Parser.init(tokens, allocator);
|
||||
var query = try parser.parse();
|
||||
defer query.deinit();
|
||||
|
||||
try std.testing.expectEqual(2, query.statements.len);
|
||||
try std.testing.expect(query.statements[0] == .match);
|
||||
try std.testing.expect(query.statements[1] == .return_stmt);
|
||||
}
|
||||
|
||||
test "Parser: path pattern" {
|
||||
const allocator = std.testing.allocator;
|
||||
const source = "MATCH (a)-[t:TRUST]->(b) RETURN a, b";
|
||||
|
||||
var lex = lexer.Lexer.init(source, allocator);
|
||||
const tokens = try lex.tokenize();
|
||||
defer allocator.free(tokens);
|
||||
|
||||
var parser = Parser.init(tokens, allocator);
|
||||
var query = try parser.parse();
|
||||
defer query.deinit();
|
||||
|
||||
try std.testing.expectEqual(1, query.statements[0].match.pattern.paths.len);
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
//! QVL Integration Layer
|
||||
//!
|
||||
//! Bridges PersistentGraph (libmdbx) with in-memory algorithms:
|
||||
//! - Load RiskGraph from disk for computation
|
||||
//! - Save results back to persistent storage
|
||||
//! - Hybrid: Cold data on disk, hot data in memory
|
||||
|
||||
const std = @import("std");
|
||||
const types = @import("types.zig");
|
||||
const storage = @import("storage.zig");
|
||||
const betrayal = @import("betrayal.zig");
|
||||
const pathfinding = @import("pathfinding.zig");
|
||||
const pop_integration = @import("pop_integration");
|
||||
|
||||
const NodeId = types.NodeId;
|
||||
const RiskEdge = types.RiskEdge;
|
||||
const RiskGraph = types.RiskGraph;
|
||||
const PersistentGraph = storage.PersistentGraph;
|
||||
const BellmanFordResult = betrayal.BellmanFordResult;
|
||||
const PathResult = pathfinding.PathResult;
|
||||
|
||||
/// Hybrid graph: persistent backing + in-memory cache
|
||||
pub const HybridGraph = struct {
|
||||
persistent: *PersistentGraph,
|
||||
cache: RiskGraph,
|
||||
cache_valid: bool,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Initialize hybrid graph
|
||||
pub fn init(persistent: *PersistentGraph, allocator: std.mem.Allocator) Self {
|
||||
return Self{
|
||||
.persistent = persistent,
|
||||
.cache = RiskGraph.init(allocator),
|
||||
.cache_valid = false,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
/// Deinitialize
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.cache.deinit();
|
||||
}
|
||||
|
||||
/// Load from persistent storage into cache
|
||||
pub fn load(self: *Self) !void {
|
||||
if (self.cache_valid) return; // Already loaded
|
||||
|
||||
// Clear existing cache
|
||||
self.cache.deinit();
|
||||
self.cache = try self.persistent.toRiskGraph(self.allocator);
|
||||
self.cache_valid = true;
|
||||
}
|
||||
|
||||
/// Save cache back to persistent storage
|
||||
pub fn save(self: *Self) !void {
|
||||
// TODO: Implement incremental save (only changed edges)
|
||||
// For now, full rewrite
|
||||
_ = self;
|
||||
}
|
||||
|
||||
/// Add edge: both cache and persistent
|
||||
pub fn addEdge(self: *Self, edge: RiskEdge) !void {
|
||||
// Add to persistent storage
|
||||
try self.persistent.addEdge(edge);
|
||||
|
||||
// Add to cache if loaded
|
||||
if (self.cache_valid) {
|
||||
try self.cache.addEdge(edge);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get outgoing neighbors (uses cache if available)
|
||||
pub fn getOutgoing(self: *Self, node: NodeId) ![]const usize {
|
||||
if (self.cache_valid) {
|
||||
return self.cache.neighbors(node);
|
||||
} else {
|
||||
// Ensure cache is loaded, then return neighbors
|
||||
try self.load();
|
||||
return self.cache.neighbors(node);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Algorithm Integration
|
||||
// =========================================================================
|
||||
|
||||
/// Run Bellman-Ford betrayal detection on persistent graph
|
||||
pub fn detectBetrayal(self: *Self, source: NodeId) !BellmanFordResult {
|
||||
try self.load(); // Ensure cache is ready
|
||||
return betrayal.detectBetrayal(&self.cache, source, self.allocator);
|
||||
}
|
||||
|
||||
/// Find trust path using A*
|
||||
pub fn findTrustPath(
|
||||
self: *Self,
|
||||
source: NodeId,
|
||||
target: NodeId,
|
||||
heuristic: pathfinding.HeuristicFn,
|
||||
heuristic_ctx: *const anyopaque,
|
||||
) !PathResult {
|
||||
try self.load();
|
||||
return pathfinding.findTrustPath(
|
||||
&self.cache, source, target, heuristic, heuristic_ctx, self.allocator);
|
||||
}
|
||||
|
||||
/// Verify Proof-of-Path and update reputation
|
||||
pub fn verifyPoP(
|
||||
self: *Self,
|
||||
proof: *const pop_integration.ProofOfPath,
|
||||
expected_receiver: [32]u8,
|
||||
expected_sender: [32]u8,
|
||||
rep_map: *pop_integration.ReputationMap,
|
||||
current_entropy: u64,
|
||||
) !pop_integration.PathVerdict {
|
||||
// This needs CompactTrustGraph, not RiskGraph...
|
||||
// Need adapter or separate implementation
|
||||
_ = self;
|
||||
_ = proof;
|
||||
_ = expected_receiver;
|
||||
_ = expected_sender;
|
||||
_ = rep_map;
|
||||
_ = current_entropy;
|
||||
@panic("TODO: Implement PoP verification for PersistentGraph");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Statistics
|
||||
// =========================================================================
|
||||
|
||||
pub fn nodeCount(self: *Self) usize {
|
||||
if (self.cache_valid) {
|
||||
return self.cache.nodeCount();
|
||||
}
|
||||
return 0; // TODO: Query from persistent
|
||||
}
|
||||
|
||||
pub fn edgeCount(self: *Self) usize {
|
||||
if (self.cache_valid) {
|
||||
return self.cache.edgeCount();
|
||||
}
|
||||
return 0; // TODO: Query from persistent
|
||||
}
|
||||
};
|
||||
|
||||
/// Transactional wrapper for batch operations
|
||||
pub const GraphTransaction = struct {
|
||||
hybrid: *HybridGraph,
|
||||
pending_edges: std.ArrayList(RiskEdge),
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn begin(hybrid: *HybridGraph, allocator: std.mem.Allocator) Self {
|
||||
return Self{
|
||||
.hybrid = hybrid,
|
||||
.pending_edges = .{}, // Empty, allocator passed on append
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.pending_edges.deinit(self.allocator);
|
||||
}
|
||||
|
||||
pub fn addEdge(self: *Self, edge: RiskEdge) !void {
|
||||
try self.pending_edges.append(self.allocator, edge);
|
||||
}
|
||||
|
||||
pub fn commit(self: *Self) !void {
|
||||
// Add all pending edges atomically
|
||||
for (self.pending_edges.items) |edge| {
|
||||
try self.hybrid.addEdge(edge);
|
||||
}
|
||||
self.pending_edges.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn rollback(self: *Self) void {
|
||||
self.pending_edges.clearRetainingCapacity();
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// TESTS
|
||||
// ============================================================================
|
||||
|
||||
test "HybridGraph: load and detect betrayal" {
|
||||
const allocator = std.testing.allocator;
|
||||
const time = @import("time");
|
||||
|
||||
const path = "/tmp/test_hybrid_db";
|
||||
defer std.fs.deleteFileAbsolute(path) catch {};
|
||||
|
||||
// Create persistent graph
|
||||
var persistent = try PersistentGraph.open(path, .{}, allocator);
|
||||
defer persistent.close();
|
||||
|
||||
// Create hybrid
|
||||
var hybrid = HybridGraph.init(&persistent, allocator);
|
||||
defer hybrid.deinit();
|
||||
|
||||
// Add edges forming negative cycle (sum of risks must be < 0)
|
||||
const ts = time.SovereignTimestamp.fromSeconds(1234567890, .system_boot);
|
||||
const expires = ts.addSeconds(86400);
|
||||
// Trust edges (negative risk = good)
|
||||
try hybrid.addEdge(.{ .from = 0, .to = 1, .risk = -0.7, .timestamp = ts, .nonce = 0, .level = 3, .expires_at = expires });
|
||||
try hybrid.addEdge(.{ .from = 1, .to = 2, .risk = -0.7, .timestamp = ts, .nonce = 1, .level = 3, .expires_at = expires });
|
||||
// Betrayal edge (high positive risk creates negative cycle)
|
||||
// -0.7 + -0.7 + 0.9 = -0.5 (negative cycle!)
|
||||
try hybrid.addEdge(.{ .from = 2, .to = 0, .risk = 0.9, .timestamp = ts, .nonce = 2, .level = 0, .expires_at = expires });
|
||||
|
||||
// Detect betrayal
|
||||
var result = try hybrid.detectBetrayal(0);
|
||||
defer result.deinit();
|
||||
|
||||
try std.testing.expect(result.betrayal_cycles.items.len > 0);
|
||||
}
|
||||
|
||||
test "GraphTransaction: commit and rollback" {
|
||||
const allocator = std.testing.allocator;
|
||||
const time = @import("time");
|
||||
|
||||
const path = "/tmp/test_tx_db";
|
||||
defer std.fs.deleteFileAbsolute(path) catch {};
|
||||
|
||||
var persistent = try PersistentGraph.open(path, .{}, allocator);
|
||||
defer persistent.close();
|
||||
|
||||
var hybrid = HybridGraph.init(&persistent, allocator);
|
||||
defer hybrid.deinit();
|
||||
|
||||
// Start transaction
|
||||
var txn = GraphTransaction.begin(&hybrid, allocator);
|
||||
defer txn.deinit();
|
||||
|
||||
// Add edges
|
||||
const ts = time.SovereignTimestamp.fromSeconds(1234567890, .system_boot);
|
||||
const expires = ts.addSeconds(86400);
|
||||
try txn.addEdge(.{ .from = 0, .to = 1, .risk = -0.3, .timestamp = ts, .nonce = 0, .level = 3, .expires_at = expires });
|
||||
try txn.addEdge(.{ .from = 1, .to = 2, .risk = -0.3, .timestamp = ts, .nonce = 1, .level = 3, .expires_at = expires });
|
||||
|
||||
// Commit
|
||||
try txn.commit();
|
||||
|
||||
// Verify edges exist
|
||||
try hybrid.load();
|
||||
try std.testing.expectEqual(hybrid.edgeCount(), 2);
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
const std = @import("std");
|
||||
const types = @import("types.zig");
|
||||
const pathfinding = @import("pathfinding.zig");
|
||||
// Import proof_of_path relative from qvl directory
|
||||
const pop = @import("../proof_of_path.zig");
|
||||
const trust_graph = @import("trust_graph");
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
//! QVL Storage Layer - Stub Implementation
|
||||
//!
|
||||
//! This is a stub/mock implementation for testing without libmdbx.
|
||||
//! Replace with real libmdbx implementation when available.
|
||||
|
||||
const std = @import("std");
|
||||
const types = @import("types.zig");
|
||||
|
||||
const NodeId = types.NodeId;
|
||||
const RiskEdge = types.RiskEdge;
|
||||
const RiskGraph = types.RiskGraph;
|
||||
|
||||
/// Mock persistent storage using in-memory HashMap
|
||||
pub const PersistentGraph = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
nodes: std.AutoHashMap(NodeId, void),
|
||||
edges: std.AutoHashMap(EdgeKey, RiskEdge),
|
||||
adjacency: std.AutoHashMap(NodeId, std.ArrayList(NodeId)),
|
||||
path: []const u8,
|
||||
|
||||
const EdgeKey = struct {
|
||||
from: NodeId,
|
||||
to: NodeId,
|
||||
|
||||
pub fn hash(self: EdgeKey) u64 {
|
||||
return @as(u64, self.from) << 32 | self.to;
|
||||
}
|
||||
|
||||
pub fn eql(self: EdgeKey, other: EdgeKey) bool {
|
||||
return self.from == other.from and self.to == other.to;
|
||||
}
|
||||
};
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Open or create persistent graph (mock: in-memory)
|
||||
pub fn open(path: []const u8, config: DBConfig, allocator: std.mem.Allocator) !Self {
|
||||
_ = config;
|
||||
return Self{
|
||||
.allocator = allocator,
|
||||
.nodes = std.AutoHashMap(NodeId, void).init(allocator),
|
||||
.edges = std.AutoHashMap(EdgeKey, RiskEdge).init(allocator),
|
||||
.adjacency = std.AutoHashMap(NodeId, std.ArrayList(NodeId)).init(allocator),
|
||||
.path = try allocator.dupe(u8, path),
|
||||
};
|
||||
}
|
||||
|
||||
/// Close database
|
||||
pub fn close(self: *Self) void {
|
||||
// Clean up adjacency lists
|
||||
var it = self.adjacency.valueIterator();
|
||||
while (it.next()) |list| {
|
||||
list.deinit(self.allocator);
|
||||
}
|
||||
self.adjacency.deinit();
|
||||
self.edges.deinit();
|
||||
self.nodes.deinit();
|
||||
self.allocator.free(self.path);
|
||||
}
|
||||
|
||||
/// Add node
|
||||
pub fn addNode(self: *Self, node: NodeId) !void {
|
||||
try self.nodes.put(node, {});
|
||||
}
|
||||
|
||||
/// Add edge
|
||||
pub fn addEdge(self: *Self, edge: RiskEdge) !void {
|
||||
// Register nodes first
|
||||
try self.nodes.put(edge.from, {});
|
||||
try self.nodes.put(edge.to, {});
|
||||
|
||||
const key = EdgeKey{ .from = edge.from, .to = edge.to };
|
||||
try self.edges.put(key, edge);
|
||||
|
||||
// Update adjacency
|
||||
const entry = try self.adjacency.getOrPut(edge.from);
|
||||
if (!entry.found_existing) {
|
||||
entry.value_ptr.* = .{}; // Empty ArrayList, allocator passed on append
|
||||
}
|
||||
try entry.value_ptr.append(self.allocator, edge.to);
|
||||
}
|
||||
|
||||
/// Get outgoing neighbors
|
||||
pub fn getOutgoing(self: *Self, node: NodeId, allocator: std.mem.Allocator) ![]NodeId {
|
||||
if (self.adjacency.get(node)) |list| {
|
||||
// Copy to new slice with provided allocator
|
||||
return allocator.dupe(NodeId, list.items);
|
||||
}
|
||||
return allocator.dupe(NodeId, &[_]NodeId{});
|
||||
}
|
||||
|
||||
/// Get specific edge
|
||||
pub fn getEdge(self: *Self, from: NodeId, to: NodeId) !?RiskEdge {
|
||||
const key = EdgeKey{ .from = from, .to = to };
|
||||
return self.edges.get(key);
|
||||
}
|
||||
|
||||
/// Load in-memory RiskGraph
|
||||
pub fn toRiskGraph(self: *Self, allocator: std.mem.Allocator) !RiskGraph {
|
||||
var graph = RiskGraph.init(allocator);
|
||||
errdefer graph.deinit();
|
||||
|
||||
// First add all nodes
|
||||
var node_it = self.nodes.keyIterator();
|
||||
while (node_it.next()) |node| {
|
||||
try graph.addNode(node.*);
|
||||
}
|
||||
|
||||
// Then add all edges
|
||||
var edge_it = self.edges.valueIterator();
|
||||
while (edge_it.next()) |edge| {
|
||||
try graph.addEdge(edge.*);
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
||||
};
|
||||
|
||||
/// Database configuration (mock accepts same config for API compatibility)
|
||||
pub const DBConfig = struct {
|
||||
max_readers: u32 = 64,
|
||||
max_dbs: u32 = 8,
|
||||
map_size: usize = 10 * 1024 * 1024,
|
||||
page_size: u32 = 4096,
|
||||
};
|
||||
|
||||
// Re-export for integration.zig
|
||||
pub const lmdb = struct {
|
||||
// Stub exports
|
||||
};
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
//! Sovereign Index: L2 Session Manager
|
||||
//!
|
||||
//! The L2 Session Manager provides cryptographically verified,
|
||||
//! resilient peer-to-peer session management for the Libertaria Stack.
|
||||
//!
|
||||
//! ## Core Concepts
|
||||
//!
|
||||
//! - **Session**: A sovereign state machine representing trust relationship
|
||||
//! - **Handshake**: PQxdh-based mutual authentication
|
||||
//! - **Heartbeat**: Cooperative liveness verification
|
||||
//! - **Rotation**: Seamless key material refresh
|
||||
//!
|
||||
//! ## Transport
|
||||
//!
|
||||
//! This module uses QUIC and μTCP (micro-transport).
|
||||
//! WebSockets are explicitly excluded by design (ADR-001).
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```janus
|
||||
//! // Establish a session
|
||||
//! let session = try l2_session.establish(
|
||||
//! peer_did: peer_identity,
|
||||
//! ctx: ctx
|
||||
//! );
|
||||
//!
|
||||
//! // Send message through session
|
||||
//! try session.send(message, ctx);
|
||||
//!
|
||||
//! // Receive with automatic decryption
|
||||
//! let response = try session.receive(timeout: 5s, ctx);
|
||||
//! ```
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! - State machine: Explicit, auditable transitions
|
||||
//! - Crypto: X25519Kyber768 hybrid (PQ-safe)
|
||||
//! - Resilience: Graceful degradation, automatic recovery
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// Public API exports
|
||||
pub const Session = @import("l2_session/session.zig").Session;
|
||||
pub const State = @import("l2_session/state.zig").State;
|
||||
pub const Handshake = @import("l2_session/handshake.zig").Handshake;
|
||||
pub const Heartbeat = @import("l2_session/heartbeat.zig").Heartbeat;
|
||||
pub const KeyRotation = @import("l2_session/rotation.zig").KeyRotation;
|
||||
pub const Transport = @import("l2_session/transport.zig").Transport;
|
||||
|
||||
// Re-export core types
|
||||
pub const SessionConfig = @import("l2_session/config.zig").SessionConfig;
|
||||
pub const SessionError = @import("l2_session/error.zig").SessionError;
|
||||
|
||||
/// Establish a new session with a peer
|
||||
///
|
||||
/// This initiates the PQxdh handshake and returns a session in
|
||||
/// the `handshake_initiated` state. The session becomes `established`
|
||||
/// after the peer responds.
|
||||
pub fn establish(
|
||||
peer_did: []const u8,
|
||||
config: SessionConfig,
|
||||
ctx: anytype,
|
||||
) !Session {
|
||||
return Handshake.initiate(peer_did, config, ctx);
|
||||
}
|
||||
|
||||
/// Resume a previously established session
|
||||
///
|
||||
/// If valid key material exists from a previous session,
|
||||
/// this reuses it for fast re-establishment.
|
||||
pub fn resume(
|
||||
peer_did: []const u8,
|
||||
stored_session: StoredSession,
|
||||
ctx: anytype,
|
||||
) !Session {
|
||||
return Handshake.resume(peer_did, stored_session, ctx);
|
||||
}
|
||||
|
||||
/// Accept an incoming session request
|
||||
///
|
||||
/// Call this when receiving a handshake request from a peer.
|
||||
pub fn accept(
|
||||
request: HandshakeRequest,
|
||||
config: SessionConfig,
|
||||
ctx: anytype,
|
||||
) !Session {
|
||||
return Handshake.respond(request, config, ctx);
|
||||
}
|
||||
|
||||
/// Process all pending session events
|
||||
///
|
||||
/// Call this periodically (e.g., in your event loop) to handle
|
||||
/// heartbeats, timeouts, and state transitions.
|
||||
pub fn tick(
|
||||
sessions: []Session,
|
||||
ctx: anytype,
|
||||
) void {
|
||||
for (sessions) |*session| {
|
||||
session.tick(ctx);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# L2 Session Manager
|
||||
|
||||
Sovereign peer-to-peer session management for Libertaria.
|
||||
|
||||
## Overview
|
||||
|
||||
The L2 Session Manager establishes and maintains cryptographically verified sessions between Libertaria nodes. It provides:
|
||||
|
||||
- **Post-quantum security** (X25519Kyber768 hybrid)
|
||||
- **Resilient state machines** (graceful degradation, automatic recovery)
|
||||
- **Seamless key rotation** (no message loss during rotation)
|
||||
- **Multi-transport support** (QUIC primary, μTCP fallback)
|
||||
|
||||
## Why No WebSockets
|
||||
|
||||
This module explicitly excludes WebSockets (see ADR-001). We use:
|
||||
|
||||
| Transport | Use Case | Advantages |
|
||||
|-----------|----------|------------|
|
||||
| **QUIC** | Primary transport | 0-RTT, built-in TLS, multiplexing |
|
||||
| **μTCP** | Fallback, legacy | Micro-optimized, minimal overhead |
|
||||
| **UDP** | Discovery, broadcast | Stateless, fast probing |
|
||||
|
||||
WebSockets add HTTP overhead, proxy complexity, and fragility. Libertaria is built for the 2030s, not the 2010s.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```janus
|
||||
// Establish session
|
||||
let session = try l2_session.establish(
|
||||
peer_did: "did:morpheus:abc123",
|
||||
config: .{ ttl: 24h, heartbeat: 30s },
|
||||
ctx: ctx
|
||||
);
|
||||
|
||||
// Use session
|
||||
try session.send(message);
|
||||
let response = try session.receive(timeout: 5s);
|
||||
```
|
||||
|
||||
## State Machine
|
||||
|
||||
```
|
||||
idle → handshake_initiated → established → degraded → suspended
|
||||
↓ ↓ ↓
|
||||
failed rotating → established
|
||||
```
|
||||
|
||||
See SPEC.md for full details.
|
||||
|
||||
## Module Structure
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `session.zig` | Core Session struct and API |
|
||||
| `state.zig` | State machine definitions and transitions |
|
||||
| `handshake.zig` | PQxdh handshake implementation |
|
||||
| `heartbeat.zig` | Keepalive and TTL management |
|
||||
| `rotation.zig` | Key rotation without interruption |
|
||||
| `transport.zig` | QUIC/μTCP abstraction layer |
|
||||
| `error.zig` | Session-specific error types |
|
||||
| `config.zig` | Configuration structures |
|
||||
|
||||
## Testing
|
||||
|
||||
Tests are colocated in `test_*.zig` files. Run with:
|
||||
|
||||
```bash
|
||||
zig build test-l2-session
|
||||
```
|
||||
|
||||
## Specification
|
||||
|
||||
Full specification in [SPEC.md](./SPEC.md).
|
||||
|
|
@ -0,0 +1,375 @@
|
|||
# SPEC-018: L2 Session Manager
|
||||
|
||||
**Status:** DRAFT
|
||||
**Version:** 0.1.0
|
||||
**Date:** 2026-02-02
|
||||
**Profile:** :service (with :core crypto primitives)
|
||||
**Supersedes:** None (New Feature)
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
The L2 Session Manager provides sovereign, cryptographically verified peer-to-peer session management for the Libertaria Stack. It establishes trust relationships, maintains them through network disruptions, and ensures post-quantum security through automatic key rotation.
|
||||
|
||||
### 1.1 Design Principles
|
||||
|
||||
1. **Explicit State**: Every session state is explicit, logged, and auditable
|
||||
2. **Graceful Degradation**: Sessions survive network partitions without data loss
|
||||
3. **No WebSockets**: Uses QUIC/μTCP only (see ADR-001)
|
||||
4. **Post-Quantum Security**: X25519Kyber768 hybrid key exchange
|
||||
|
||||
### 1.2 Transport Architecture
|
||||
|
||||
| Transport | Role | Protocol Details |
|
||||
|-----------|------|------------------|
|
||||
| QUIC | Primary | UDP-based, 0-RTT, TLS 1.3 built-in |
|
||||
| μTCP | Fallback | Micro-optimized TCP, minimal overhead |
|
||||
| Raw UDP | Discovery | Stateless probing, STUN-like |
|
||||
|
||||
**Rationale**: WebSockets (RFC 6455) are excluded. They add HTTP handshake overhead, require proxy support, and don't support UDP hole punching natively.
|
||||
|
||||
---
|
||||
|
||||
## 2. Behavioral Specification (BDD)
|
||||
|
||||
### 2.1 Session Establishment
|
||||
|
||||
```gherkin
|
||||
Feature: Session Establishment
|
||||
|
||||
Scenario: Successful establishment with new peer
|
||||
Given a discovered peer with valid DID
|
||||
When session establishment is initiated
|
||||
Then state transitions to "handshake_initiated"
|
||||
And PQxdh handshake request is sent
|
||||
When valid handshake response received
|
||||
Then state transitions to "established"
|
||||
And shared session keys are derived
|
||||
And TTL is set to 24 hours
|
||||
|
||||
Scenario: Session resumption
|
||||
Given previous session exists with unchanged prekeys
|
||||
When resumption is initiated
|
||||
Then existing key material is reused
|
||||
And state becomes "established" within 100ms
|
||||
|
||||
Scenario: Establishment timeout
|
||||
When no response within 5 seconds
|
||||
Then state transitions to "failed"
|
||||
And failure reason is "timeout"
|
||||
And retry is scheduled with exponential backoff
|
||||
|
||||
Scenario: Authentication failure
|
||||
When invalid signature received
|
||||
Then state transitions to "failed"
|
||||
And failure reason is "authentication_failed"
|
||||
And peer is quarantined for 60 seconds
|
||||
```
|
||||
|
||||
### 2.2 Session Maintenance
|
||||
|
||||
```gherkin
|
||||
Feature: Session Maintenance
|
||||
|
||||
Scenario: Heartbeat success
|
||||
When 30 seconds pass without activity
|
||||
Then heartbeat is sent
|
||||
And peer responds within 2 seconds
|
||||
And TTL is extended
|
||||
|
||||
Scenario: Single missed heartbeat
|
||||
Given peer misses 1 heartbeat
|
||||
When next heartbeat succeeds
|
||||
Then session remains "established"
|
||||
And warning is logged
|
||||
|
||||
Scenario: Session suspension
|
||||
Given peer misses 3 heartbeats
|
||||
When third timeout occurs
|
||||
Then state becomes "suspended"
|
||||
And queued messages are held
|
||||
And recovery is attempted after 60s
|
||||
|
||||
Scenario: Automatic key rotation
|
||||
Given session age reaches 24 hours
|
||||
When rotation window triggers
|
||||
Then new ephemeral keys are generated
|
||||
And re-handshake is initiated
|
||||
And no messages are lost
|
||||
```
|
||||
|
||||
### 2.3 Degradation and Recovery
|
||||
|
||||
```gherkin
|
||||
Feature: Degradation and Recovery
|
||||
|
||||
Scenario: Network partition detection
|
||||
When connectivity lost for >30s
|
||||
Then state becomes "degraded"
|
||||
And messages are queued
|
||||
And session is preserved
|
||||
|
||||
Scenario: Partition recovery
|
||||
Given session is "degraded"
|
||||
When connectivity restored
|
||||
Then re-establishment is attempted
|
||||
And queued messages are flushed
|
||||
|
||||
Scenario: Transport fallback
|
||||
Given session over QUIC
|
||||
When QUIC fails
|
||||
Then re-establishment over μTCP is attempted
|
||||
And this is transparent to upper layers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. State Machine
|
||||
|
||||
### 3.1 State Definitions
|
||||
|
||||
| State | Description | Valid Transitions |
|
||||
|-------|-------------|-------------------|
|
||||
| `idle` | Initial state | `handshake_initiated`, `handshake_received` |
|
||||
| `handshake_initiated` | Awaiting response | `established`, `failed` |
|
||||
| `handshake_received` | Received request, preparing response | `established`, `failed` |
|
||||
| `established` | Active session | `degraded`, `rotating` |
|
||||
| `degraded` | Connectivity issues | `established`, `suspended` |
|
||||
| `rotating` | Key rotation in progress | `established`, `failed` |
|
||||
| `suspended` | Extended failure | `[cleanup]`, `handshake_initiated` |
|
||||
| `failed` | Terminal failure | `[cleanup]`, `handshake_initiated` (retry) |
|
||||
|
||||
### 3.2 State Diagram
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> idle
|
||||
|
||||
idle --> handshake_initiated: initiate_handshake()
|
||||
idle --> handshake_received: receive_handshake()
|
||||
|
||||
handshake_initiated --> established: receive_valid_response()
|
||||
handshake_initiated --> failed: timeout / invalid_sig
|
||||
|
||||
handshake_received --> established: send_response + ack
|
||||
handshake_received --> failed: timeout
|
||||
|
||||
established --> degraded: missed_heartbeats(3)
|
||||
established --> rotating: time_to_rotate()
|
||||
|
||||
degraded --> established: connectivity_restored
|
||||
degraded --> suspended: timeout(60s)
|
||||
|
||||
suspended --> [*]: cleanup()
|
||||
suspended --> handshake_initiated: retry()
|
||||
|
||||
rotating --> established: rotation_complete
|
||||
rotating --> failed: rotation_timeout
|
||||
|
||||
failed --> [*]: cleanup()
|
||||
failed --> handshake_initiated: retry_with_backoff()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Architecture Decision Records
|
||||
|
||||
### ADR-001: No WebSockets
|
||||
|
||||
**Context:** P2P systems need reliable, low-latency, firewall-traversing transport.
|
||||
|
||||
**Decision:** Exclude WebSockets. Use QUIC as primary, μTCP as fallback.
|
||||
|
||||
**Consequences:**
|
||||
- ✅ Zero HTTP overhead
|
||||
- ✅ Native UDP hole punching
|
||||
- ✅ 0-RTT connection establishment
|
||||
- ✅ Built-in TLS 1.3 (QUIC)
|
||||
- ❌ No browser compatibility (acceptable — native-first design)
|
||||
- ❌ Corporate proxy issues (mitigation: relay mode)
|
||||
|
||||
### ADR-002: State Machine Over Connection Object
|
||||
|
||||
**Context:** Traditional "connections" are ephemeral and error-prone.
|
||||
|
||||
**Decision:** Model sessions as explicit state machines with cryptographic verification.
|
||||
|
||||
**Consequences:**
|
||||
- ✅ Every transition is auditable
|
||||
- ✅ Supports offline-to-online continuity
|
||||
- ✅ Enables split-world scenarios
|
||||
- ❌ Higher cognitive load (mitigation: tooling)
|
||||
|
||||
### ADR-003: Post-Quantum Hybrid
|
||||
|
||||
**Context:** PQ crypto is slow; classical may be broken by 2035.
|
||||
|
||||
**Decision:** X25519Kyber768 hybrid key exchange.
|
||||
|
||||
**Consequences:**
|
||||
- ✅ Resistant to classical and quantum attacks
|
||||
- ✅ Hardware acceleration for X25519
|
||||
- ❌ Larger handshake packets
|
||||
|
||||
---
|
||||
|
||||
## 5. Interface Specification
|
||||
|
||||
### 5.1 Core Types
|
||||
|
||||
```janus
|
||||
/// Session configuration
|
||||
const SessionConfig = struct {
|
||||
/// Time-to-live before requiring re-handshake
|
||||
ttl: Duration = 24h,
|
||||
|
||||
/// Heartbeat interval
|
||||
heartbeat_interval: Duration = 30s,
|
||||
|
||||
/// Missed heartbeats before degradation
|
||||
heartbeat_tolerance: u8 = 3,
|
||||
|
||||
/// Handshake timeout
|
||||
handshake_timeout: Duration = 5s,
|
||||
|
||||
/// Key rotation window (before TTL expires)
|
||||
rotation_window: Duration = 1h,
|
||||
};
|
||||
|
||||
/// Session state enumeration
|
||||
const State = enum {
|
||||
idle,
|
||||
handshake_initiated,
|
||||
handshake_received,
|
||||
established,
|
||||
degraded,
|
||||
rotating,
|
||||
suspended,
|
||||
failed,
|
||||
};
|
||||
|
||||
/// Session error types
|
||||
const SessionError = !union {
|
||||
Timeout,
|
||||
AuthenticationFailed,
|
||||
TransportFailed,
|
||||
KeyRotationFailed,
|
||||
InvalidState,
|
||||
};
|
||||
```
|
||||
|
||||
### 5.2 Public API
|
||||
|
||||
```janus
|
||||
/// Establish new session
|
||||
func establish(
|
||||
peer_did: []const u8,
|
||||
config: SessionConfig,
|
||||
ctx: Context
|
||||
) !Session
|
||||
with ctx where ctx.has(
|
||||
.net_connect,
|
||||
.crypto_pqxdh,
|
||||
.did_resolve,
|
||||
.time
|
||||
);
|
||||
|
||||
/// Resume existing session
|
||||
func resume(
|
||||
peer_did: []const u8,
|
||||
stored: StoredSession,
|
||||
ctx: Context
|
||||
) !Session;
|
||||
|
||||
/// Accept incoming session
|
||||
func accept(
|
||||
request: HandshakeRequest,
|
||||
config: SessionConfig,
|
||||
ctx: Context
|
||||
) !Session;
|
||||
|
||||
/// Process all sessions (call in event loop)
|
||||
func tick(sessions: []Session, ctx: Context) void;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Testing Requirements
|
||||
|
||||
### 6.1 Unit Tests
|
||||
|
||||
All Gherkin scenarios must have corresponding tests:
|
||||
|
||||
```janus
|
||||
test "Scenario-001.1: Session establishes successfully" do
|
||||
// Validates: SPEC-018 2.1 SCENARIO-1
|
||||
let session = try Session.establish(test_peer, test_config, ctx);
|
||||
assert(session.state == .handshake_initiated);
|
||||
// ... simulate response
|
||||
assert(session.state == .established);
|
||||
end
|
||||
```
|
||||
|
||||
### 6.2 Integration Tests
|
||||
|
||||
- Two-node handshake with real crypto
|
||||
- Network partition simulation
|
||||
- Transport fallback verification
|
||||
- Chaos testing (random packet loss)
|
||||
|
||||
### 6.3 Mock Interfaces
|
||||
|
||||
| Dependency | Mock Interface |
|
||||
|------------|----------------|
|
||||
| L0 Transport | `MockTransport` with latency/packet loss controls |
|
||||
| PQxdh | Deterministic test vectors |
|
||||
| Clock | Injectable `TimeSource` |
|
||||
| DID Resolver | `MockResolver` with test documents |
|
||||
|
||||
---
|
||||
|
||||
## 7. Security Considerations
|
||||
|
||||
### 7.1 Threat Model
|
||||
|
||||
| Threat | Mitigation |
|
||||
|--------|------------|
|
||||
| Man-in-the-middle | PQxdh with DID-based identity |
|
||||
| Replay attacks | Monotonic counters in heartbeats |
|
||||
| Key compromise | Automatic rotation every 24h |
|
||||
| Timing attacks | Constant-time crypto operations |
|
||||
| Denial of service | Quarantine + exponential backoff |
|
||||
|
||||
### 7.2 Cryptographic Requirements
|
||||
|
||||
- Key exchange: X25519Kyber768 (hybrid)
|
||||
- Signatures: Ed25519
|
||||
- Symmetric encryption: ChaCha20-Poly1305
|
||||
- Hashing: BLAKE3
|
||||
|
||||
---
|
||||
|
||||
## 8. Related Specifications
|
||||
|
||||
- **SPEC-017**: Janus Language Syntax
|
||||
- **RSP-1**: Registry Sovereignty Protocol
|
||||
- **RFC-0000**: Libertaria Wire Frame Protocol (L0)
|
||||
- **RFC-NCP-001**: Nexus Context Protocol
|
||||
|
||||
---
|
||||
|
||||
## 9. Rejection Criteria
|
||||
|
||||
This specification is NOT READY until:
|
||||
- [ ] All Gherkin scenarios have TDD tests
|
||||
- [ ] Mermaid diagrams are validated
|
||||
- [ ] ADR-001 is acknowledged by both Architects
|
||||
- [ ] Mock interfaces are defined
|
||||
- [ ] Security review complete
|
||||
|
||||
---
|
||||
|
||||
**Sovereign Index**: `l2_session.zig`
|
||||
**Feature Folder**: `l2_session/`
|
||||
**Status**: AWAITING ACKNOWLEDGMENT
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
//! Session configuration
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// Session configuration
|
||||
pub const SessionConfig = struct {
|
||||
/// Time-to-live before requiring re-handshake
|
||||
ttl: Duration = .{ .hours = 24 },
|
||||
|
||||
/// Heartbeat interval
|
||||
heartbeat_interval: Duration = .{ .seconds = 30 },
|
||||
|
||||
/// Missed heartbeats before degradation
|
||||
heartbeat_tolerance: u8 = 3,
|
||||
|
||||
/// Handshake timeout
|
||||
handshake_timeout: Duration = .{ .seconds = 5 },
|
||||
|
||||
/// Key rotation window (before TTL expires)
|
||||
rotation_window: Duration = .{ .hours = 1 },
|
||||
};
|
||||
|
||||
/// Duration helper
|
||||
pub const Duration = struct {
|
||||
seconds: u64 = 0,
|
||||
minutes: u64 = 0,
|
||||
hours: u64 = 0,
|
||||
|
||||
pub fn seconds(self: Duration) i64 {
|
||||
return @intCast(self.seconds + self.minutes * 60 + self.hours * 3600);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
//! Session error types
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// Session-specific errors
|
||||
pub const SessionError = error{
|
||||
/// Operation timed out
|
||||
Timeout,
|
||||
|
||||
/// Peer authentication failed
|
||||
AuthenticationFailed,
|
||||
|
||||
/// Transport layer failure
|
||||
TransportFailed,
|
||||
|
||||
/// Key rotation failed
|
||||
KeyRotationFailed,
|
||||
|
||||
/// Invalid state for operation
|
||||
InvalidState,
|
||||
|
||||
/// Session expired
|
||||
SessionExpired,
|
||||
|
||||
/// Quota exceeded
|
||||
QuotaExceeded,
|
||||
};
|
||||
|
||||
/// Failure reasons for telemetry
|
||||
pub const FailureReason = enum {
|
||||
timeout,
|
||||
authentication_failed,
|
||||
transport_error,
|
||||
protocol_violation,
|
||||
key_rotation_timeout,
|
||||
session_expired,
|
||||
};
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
//! PQxdh handshake implementation
|
||||
//!
|
||||
//! Implements X25519Kyber768 hybrid key exchange for post-quantum security.
|
||||
|
||||
const std = @import("std");
|
||||
const Session = @import("session.zig").Session;
|
||||
const SessionConfig = @import("config.zig").SessionConfig;
|
||||
|
||||
/// Handshake state machine
|
||||
pub const Handshake = struct {
|
||||
/// Initiate handshake as client
|
||||
pub fn initiate(
|
||||
peer_did: []const u8,
|
||||
config: SessionConfig,
|
||||
ctx: anytype,
|
||||
) !Session {
|
||||
// TODO: Implement PQxdh initiation
|
||||
_ = peer_did;
|
||||
_ = config;
|
||||
_ = ctx;
|
||||
|
||||
var session = Session.new(peer_did, config);
|
||||
session.state = .handshake_initiated;
|
||||
return session;
|
||||
}
|
||||
|
||||
/// Resume existing session
|
||||
pub fn resume(
|
||||
peer_did: []const u8,
|
||||
stored: StoredSession,
|
||||
ctx: anytype,
|
||||
) !Session {
|
||||
// TODO: Implement fast resumption
|
||||
_ = peer_did;
|
||||
_ = stored;
|
||||
_ = ctx;
|
||||
|
||||
return Session.new(peer_did, .{});
|
||||
}
|
||||
|
||||
/// Respond to handshake as server
|
||||
pub fn respond(
|
||||
request: HandshakeRequest,
|
||||
config: SessionConfig,
|
||||
ctx: anytype,
|
||||
) !Session {
|
||||
// TODO: Implement PQxdh response
|
||||
_ = request;
|
||||
_ = config;
|
||||
_ = ctx;
|
||||
|
||||
return Session.new("", config);
|
||||
}
|
||||
};
|
||||
|
||||
/// Incoming handshake request
|
||||
pub const HandshakeRequest = struct {
|
||||
peer_did: []const u8,
|
||||
ephemeral_pubkey: []const u8,
|
||||
prekey_id: u64,
|
||||
signature: [64]u8,
|
||||
};
|
||||
|
||||
/// Stored session for resumption
|
||||
const StoredSession = @import("session.zig").StoredSession;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
//! Heartbeat and TTL management
|
||||
//!
|
||||
//! Keeps sessions alive through cooperative heartbeats.
|
||||
|
||||
const std = @import("std");
|
||||
const Session = @import("session.zig").Session;
|
||||
|
||||
/// Heartbeat manager
|
||||
pub const Heartbeat = struct {
|
||||
/// Send a heartbeat to the peer
|
||||
pub fn send(session: *Session, ctx: anytype) !void {
|
||||
// TODO: Implement heartbeat sending
|
||||
_ = session;
|
||||
_ = ctx;
|
||||
}
|
||||
|
||||
/// Process received heartbeat
|
||||
pub fn receive(session: *Session, ctx: anytype) !void {
|
||||
// TODO: Update last_activity, reset missed count
|
||||
_ = session;
|
||||
_ = ctx;
|
||||
}
|
||||
|
||||
/// Check if heartbeat is due
|
||||
pub fn isDue(session: *Session, now: i64) bool {
|
||||
const elapsed = now - session.last_activity;
|
||||
return elapsed >= session.config.heartbeat_interval.seconds();
|
||||
}
|
||||
|
||||
/// Handle missed heartbeat
|
||||
pub fn handleMissed(session: *Session) void {
|
||||
session.missed_heartbeats += 1;
|
||||
|
||||
if (session.missed_heartbeats >= session.config.heartbeat_tolerance) {
|
||||
// Transition to degraded state
|
||||
session.state = .degraded;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
//! Key rotation without service interruption
|
||||
//!
|
||||
//! Seamlessly rotates session keys before TTL expiration.
|
||||
|
||||
const std = @import("std");
|
||||
const Session = @import("session.zig").Session;
|
||||
|
||||
/// Key rotation manager
|
||||
pub const KeyRotation = struct {
|
||||
/// Check if rotation is needed
|
||||
pub fn isNeeded(session: *Session, now: i64) bool {
|
||||
const time_to_expiry = session.ttl_deadline - now;
|
||||
return time_to_expiry <= session.config.rotation_window.seconds();
|
||||
}
|
||||
|
||||
/// Initiate key rotation
|
||||
pub fn initiate(session: *Session, ctx: anytype) !void {
|
||||
// TODO: Generate new ephemeral keys
|
||||
// TODO: Initiate re-handshake
|
||||
_ = session;
|
||||
_ = ctx;
|
||||
}
|
||||
|
||||
/// Complete rotation with new keys
|
||||
pub fn complete(session: *Session, new_keys: SessionKeys) void {
|
||||
// TODO: Atomically swap keys
|
||||
// TODO: Update TTL
|
||||
_ = session;
|
||||
_ = new_keys;
|
||||
}
|
||||
};
|
||||
|
||||
const SessionKeys = @import("session.zig").SessionKeys;
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
//! Session struct and core API
|
||||
//!
|
||||
//! The Session is the primary interface for L2 peer communication.
|
||||
|
||||
const std = @import("std");
|
||||
const State = @import("state.zig").State;
|
||||
const SessionConfig = @import("config.zig").SessionConfig;
|
||||
const SessionError = @import("error.zig").SessionError;
|
||||
|
||||
/// A sovereign session with a peer
|
||||
///
|
||||
/// Sessions are state machines that manage the lifecycle of a
|
||||
/// cryptographically verified peer relationship.
|
||||
pub const Session = struct {
|
||||
/// Peer DID (decentralized identifier)
|
||||
peer_did: []const u8,
|
||||
|
||||
/// Current state in the state machine
|
||||
state: State,
|
||||
|
||||
/// Configuration
|
||||
config: SessionConfig,
|
||||
|
||||
/// Session keys (post-handshake)
|
||||
keys: ?SessionKeys,
|
||||
|
||||
/// Creation timestamp
|
||||
created_at: i64,
|
||||
|
||||
/// Last activity timestamp
|
||||
last_activity: i64,
|
||||
|
||||
/// TTL deadline
|
||||
ttl_deadline: i64,
|
||||
|
||||
/// Heartbeat tracking
|
||||
missed_heartbeats: u8,
|
||||
|
||||
/// Retry tracking
|
||||
retry_count: u8,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Create a new session in idle state
|
||||
pub fn new(peer_did: []const u8, config: SessionConfig) Self {
|
||||
const now = std.time.timestamp();
|
||||
return .{
|
||||
.peer_did = peer_did,
|
||||
.state = .idle,
|
||||
.config = config,
|
||||
.keys = null,
|
||||
.created_at = now,
|
||||
.last_activity = now,
|
||||
.ttl_deadline = now + config.ttl.seconds(),
|
||||
.missed_heartbeats = 0,
|
||||
.retry_count = 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Process one tick of the state machine
|
||||
/// Call this regularly from your event loop
|
||||
pub fn tick(self: *Self, ctx: anytype) void {
|
||||
// TODO: Implement state machine transitions
|
||||
_ = self;
|
||||
_ = ctx;
|
||||
}
|
||||
|
||||
/// Send a message through this session
|
||||
pub fn send(self: *Self, message: []const u8, ctx: anytype) !void {
|
||||
// TODO: Implement encryption and transmission
|
||||
_ = self;
|
||||
_ = message;
|
||||
_ = ctx;
|
||||
}
|
||||
|
||||
/// Receive a message from this session
|
||||
pub fn receive(self: *Self, timeout_ms: u32, ctx: anytype) ![]const u8 {
|
||||
// TODO: Implement reception and decryption
|
||||
_ = self;
|
||||
_ = timeout_ms;
|
||||
_ = ctx;
|
||||
return &[]const u8{};
|
||||
}
|
||||
};
|
||||
|
||||
/// Session encryption keys (derived from PQxdh)
|
||||
const SessionKeys = struct {
|
||||
/// Encryption key (ChaCha20-Poly1305)
|
||||
enc_key: [32]u8,
|
||||
|
||||
/// Decryption key
|
||||
dec_key: [32]u8,
|
||||
|
||||
/// Authentication key for heartbeats
|
||||
auth_key: [32]u8,
|
||||
};
|
||||
|
||||
/// Stored session data for persistence
|
||||
pub const StoredSession = struct {
|
||||
peer_did: []const u8,
|
||||
keys: SessionKeys,
|
||||
created_at: i64,
|
||||
};
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
//! State machine definitions for L2 sessions
|
||||
//!
|
||||
//! States represent the lifecycle of a peer relationship.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// Session states
|
||||
///
|
||||
/// See SPEC.md for full state diagram and transition rules.
|
||||
pub const State = enum {
|
||||
/// Initial state
|
||||
idle,
|
||||
|
||||
/// Handshake initiated, awaiting response
|
||||
handshake_initiated,
|
||||
|
||||
/// Handshake received, preparing response
|
||||
handshake_received,
|
||||
|
||||
/// Active, healthy session
|
||||
established,
|
||||
|
||||
/// Connectivity issues detected
|
||||
degraded,
|
||||
|
||||
/// Key rotation in progress
|
||||
rotating,
|
||||
|
||||
/// Extended failure, pending cleanup or retry
|
||||
suspended,
|
||||
|
||||
/// Terminal failure state
|
||||
failed,
|
||||
|
||||
/// Check if this state allows sending messages
|
||||
pub fn canSend(self: State) bool {
|
||||
return switch (self) {
|
||||
.established, .degraded, .rotating => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if this state allows receiving messages
|
||||
pub fn canReceive(self: State) bool {
|
||||
return switch (self) {
|
||||
.established, .degraded, .rotating, .handshake_received => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if this is a terminal state
|
||||
pub fn isTerminal(self: State) bool {
|
||||
return switch (self) {
|
||||
.suspended, .failed => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// State transition events
|
||||
pub const Event = enum {
|
||||
initiate_handshake,
|
||||
receive_handshake,
|
||||
receive_response,
|
||||
send_response,
|
||||
receive_ack,
|
||||
heartbeat_ok,
|
||||
heartbeat_missed,
|
||||
timeout,
|
||||
connectivity_restored,
|
||||
time_to_rotate,
|
||||
rotation_complete,
|
||||
rotation_timeout,
|
||||
invalid_signature,
|
||||
cleanup,
|
||||
retry,
|
||||
};
|
||||
|
||||
/// Attempt state transition
|
||||
/// Returns new state or null if transition is invalid
|
||||
pub fn transition(current: State, event: Event) ?State {
|
||||
return switch (current) {
|
||||
.idle => switch (event) {
|
||||
.initiate_handshake => .handshake_initiated,
|
||||
.receive_handshake => .handshake_received,
|
||||
else => null,
|
||||
},
|
||||
|
||||
.handshake_initiated => switch (event) {
|
||||
.receive_response => .established,
|
||||
.timeout => .failed,
|
||||
.invalid_signature => .failed,
|
||||
else => null,
|
||||
},
|
||||
|
||||
.handshake_received => switch (event) {
|
||||
.send_response => .established,
|
||||
.timeout => .failed,
|
||||
else => null,
|
||||
},
|
||||
|
||||
.established => switch (event) {
|
||||
.heartbeat_missed => .degraded,
|
||||
.time_to_rotate => .rotating,
|
||||
else => null,
|
||||
},
|
||||
|
||||
.degraded => switch (event) {
|
||||
.connectivity_restored => .established,
|
||||
.timeout => .suspended,
|
||||
else => null,
|
||||
},
|
||||
|
||||
.rotating => switch (event) {
|
||||
.rotation_complete => .established,
|
||||
.rotation_timeout => .failed,
|
||||
else => null,
|
||||
},
|
||||
|
||||
.suspended => switch (event) {
|
||||
.cleanup => null, // Terminal
|
||||
.retry => .handshake_initiated,
|
||||
else => null,
|
||||
},
|
||||
|
||||
.failed => switch (event) {
|
||||
.cleanup => null, // Terminal
|
||||
.retry => .handshake_initiated,
|
||||
else => null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
//! Tests for session establishment
|
||||
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
const Session = @import("session.zig").Session;
|
||||
const State = @import("state.zig").State;
|
||||
const SessionConfig = @import("config.zig").SessionConfig;
|
||||
const Handshake = @import("handshake.zig").Handshake;
|
||||
|
||||
/// Scenario-001.1: Successful session establishment
|
||||
test "Scenario-001.1: Session establishment creates valid session" do
|
||||
// Validates: SPEC-018 2.1
|
||||
const config = SessionConfig{};
|
||||
const ctx = .{}; // Mock context
|
||||
|
||||
// In real implementation, this would perform PQxdh handshake
|
||||
// For now, we test the structure
|
||||
const session = Session.new("did:morpheus:test123", config);
|
||||
|
||||
try testing.expectEqualStrings("did:morpheus:test123", session.peer_did);
|
||||
try testing.expectEqual(State.idle, session.state);
|
||||
try testing.expect(session.created_at > 0);
|
||||
end
|
||||
|
||||
/// Scenario-001.4: Invalid signature handling
|
||||
test "Scenario-001.4: Invalid signature quarantines peer" do
|
||||
// Validates: SPEC-018 2.1
|
||||
// TODO: Implement with mock crypto
|
||||
const config = SessionConfig{};
|
||||
var session = Session.new("did:morpheus:badactor", config);
|
||||
|
||||
// Simulate failed authentication
|
||||
session.state = State.failed;
|
||||
|
||||
// TODO: Verify quarantine is set
|
||||
try testing.expectEqual(State.failed, session.state);
|
||||
end
|
||||
|
||||
/// Test session configuration defaults
|
||||
test "Default configuration is valid" do
|
||||
const config = SessionConfig{};
|
||||
|
||||
try testing.expectEqual(@as(u64, 24), config.ttl.hours);
|
||||
try testing.expectEqual(@as(u64, 30), config.heartbeat_interval.seconds);
|
||||
try testing.expectEqual(@as(u8, 3), config.heartbeat_tolerance);
|
||||
try testing.expectEqual(@as(u64, 5), config.handshake_timeout.seconds);
|
||||
end
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
//! Tests for session state machine
|
||||
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
const Session = @import("session.zig").Session;
|
||||
const State = @import("state.zig").State;
|
||||
const transition = @import("state.zig").transition;
|
||||
const Event = @import("state.zig").Event;
|
||||
const SessionConfig = @import("config.zig").SessionConfig;
|
||||
|
||||
/// Scenario-001.1: Session transitions from idle to handshake_initiated
|
||||
test "Scenario-001.1: Session transitions correctly" do
|
||||
// Validates: SPEC-018 2.1
|
||||
const config = SessionConfig{};
|
||||
var session = Session.new("did:test:123", config);
|
||||
|
||||
try testing.expectEqual(State.idle, session.state);
|
||||
|
||||
session.state = transition(session.state, .initiate_handshake).?;
|
||||
try testing.expectEqual(State.handshake_initiated, session.state);
|
||||
end
|
||||
|
||||
/// Scenario-001.3: Session fails after timeout
|
||||
test "Scenario-001.3: Timeout leads to failed state" do
|
||||
// Validates: SPEC-018 2.1
|
||||
const config = SessionConfig{};
|
||||
var session = Session.new("did:test:456", config);
|
||||
|
||||
session.state = transition(session.state, .initiate_handshake).?;
|
||||
try testing.expectEqual(State.handshake_initiated, session.state);
|
||||
|
||||
session.state = transition(session.state, .timeout).?;
|
||||
try testing.expectEqual(State.failed, session.state);
|
||||
end
|
||||
|
||||
/// Scenario-002.1: Heartbeat extends session TTL
|
||||
test "Scenario-002.1: Heartbeat extends TTL" do
|
||||
// Validates: SPEC-018 2.2
|
||||
const config = SessionConfig{};
|
||||
var session = Session.new("did:test:abc", config);
|
||||
|
||||
// Simulate established state
|
||||
session.state = .established;
|
||||
const original_ttl = session.ttl_deadline;
|
||||
|
||||
// Simulate heartbeat
|
||||
session.last_activity = std.time.timestamp();
|
||||
session.ttl_deadline = session.last_activity + config.ttl.seconds();
|
||||
|
||||
try testing.expect(session.ttl_deadline > original_ttl);
|
||||
try testing.expectEqual(State.established, session.state);
|
||||
end
|
||||
|
||||
/// Test state transition matrix
|
||||
test "All valid transitions work" do
|
||||
// idle -> handshake_initiated
|
||||
try testing.expectEqual(
|
||||
State.handshake_initiated,
|
||||
transition(.idle, .initiate_handshake)
|
||||
);
|
||||
|
||||
// handshake_initiated -> established
|
||||
try testing.expectEqual(
|
||||
State.established,
|
||||
transition(.handshake_initiated, .receive_response)
|
||||
);
|
||||
|
||||
// established -> degraded
|
||||
try testing.expectEqual(
|
||||
State.degraded,
|
||||
transition(.established, .heartbeat_missed)
|
||||
);
|
||||
|
||||
// degraded -> established
|
||||
try testing.expectEqual(
|
||||
State.established,
|
||||
transition(.degraded, .connectivity_restored)
|
||||
);
|
||||
end
|
||||
|
||||
/// Test invalid transitions return null
|
||||
test "Invalid transitions return null" do
|
||||
// idle cannot go to established directly
|
||||
try testing.expectEqual(null, transition(.idle, .receive_response));
|
||||
|
||||
// established cannot go to idle
|
||||
try testing.expectEqual(null, transition(.established, .initiate_handshake));
|
||||
|
||||
// failed is terminal (no transitions)
|
||||
try testing.expectEqual(null, transition(.failed, .heartbeat_ok));
|
||||
end
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
//! Transport abstraction (QUIC / μTCP)
|
||||
//!
|
||||
//! No WebSockets. See ADR-001.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// Transport abstraction
|
||||
pub const Transport = struct {
|
||||
/// Send data to peer
|
||||
pub fn send(data: []const u8, ctx: anytype) !void {
|
||||
// TODO: Implement QUIC primary, μTCP fallback
|
||||
_ = data;
|
||||
_ = ctx;
|
||||
}
|
||||
|
||||
/// Receive data from peer
|
||||
pub fn receive(timeout_ms: u32, ctx: anytype) !?[]const u8 {
|
||||
// TODO: Implement reception
|
||||
_ = timeout_ms;
|
||||
_ = ctx;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
## NCP Core Types
|
||||
|
||||
This directory contains the Nexus Context Protocol prototype implementation.
|
||||
|
||||
### Structure
|
||||
|
||||
- `src/types.nim` - Core types (CID, ContextNode, Path)
|
||||
- `src/l0_storage.nim` - File backend, CID generation (Blake3)
|
||||
- `src/l1_index.nim` - B-Tree index, path-based addressing
|
||||
- `tests/test_ncp.nim` - Unit tests
|
||||
|
||||
### Status
|
||||
|
||||
Feature 1 (Core Types): In Progress
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
## l0_storage.nim: L0 Storage Layer for NCP
|
||||
## File-based backend with CID content addressing
|
||||
## RFC-NCP-001 Implementation
|
||||
|
||||
import std/[os, paths, sequtils, hashes]
|
||||
import types
|
||||
|
||||
## Storage Configuration
|
||||
type StorageConfig* = object
|
||||
rootPath*: string ## Root directory for storage
|
||||
maxFileSize*: int64 ## Max file size (default: 100MB)
|
||||
compression*: bool ## Enable compression (future)
|
||||
|
||||
## L0 Storage Handle
|
||||
type L0Storage* = object
|
||||
config*: StorageConfig
|
||||
root*: string
|
||||
|
||||
proc initL0Storage*(rootPath: string): L0Storage =
|
||||
## Initialize L0 Storage with root directory
|
||||
result.config.rootPath = rootPath
|
||||
result.config.maxFileSize = 100 * 1024 * 1024 # 100MB
|
||||
result.root = rootPath
|
||||
|
||||
# Ensure directory exists
|
||||
createDir(rootPath)
|
||||
|
||||
## CID to file path mapping
|
||||
## CID: [0x12, 0x34, 0x56, ...] -> path: "root/12/34/5678..."
|
||||
proc cidToPath*(storage: L0Storage, cid: CID): string =
|
||||
## Convert CID to filesystem path (content-addressed)
|
||||
let hex = cid.mapIt(it.toHex(2)).join()
|
||||
result = storage.root / hex[0..1] / hex[2..3] / hex[4..^1]
|
||||
|
||||
## Store data and return CID
|
||||
proc store*(storage: L0Storage, data: openArray[byte]): CID =
|
||||
## Store raw data, return CID
|
||||
let cid = generateCID(data)
|
||||
let path = storage.cidToPath(cid)
|
||||
|
||||
# Create directory structure
|
||||
createDir(parentDir(path))
|
||||
|
||||
# Write data
|
||||
writeFile(path, data)
|
||||
|
||||
return cid
|
||||
|
||||
## Retrieve data by CID
|
||||
proc retrieve*(storage: L0Storage, cid: CID): seq[byte] =
|
||||
## Retrieve data by CID
|
||||
let path = storage.cidToPath(cid)
|
||||
if fileExists(path):
|
||||
result = readFile(path).toSeq.mapIt(byte(it))
|
||||
else:
|
||||
result = @[] # Not found
|
||||
|
||||
## Check if CID exists
|
||||
proc exists*(storage: L0Storage, cid: CID): bool =
|
||||
## Check if content exists
|
||||
let path = storage.cidToPath(cid)
|
||||
return fileExists(path)
|
||||
|
||||
## Delete content by CID
|
||||
proc delete*(storage: L0Storage, cid: CID): bool =
|
||||
## Delete content, return success
|
||||
let path = storage.cidToPath(cid)
|
||||
if fileExists(path):
|
||||
removeFile(path)
|
||||
return true
|
||||
return false
|
||||
|
||||
## Export
|
||||
export L0Storage, StorageConfig
|
||||
export initL0Storage, cidToPath, store, retrieve, exists, delete
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
## l1_index.nim: L1 Index Layer for NCP
|
||||
## B-Tree index for path-based addressing
|
||||
## RFC-NCP-001 Implementation
|
||||
|
||||
import std/[tables, sequtils, algorithm, strutils]
|
||||
import types
|
||||
|
||||
## Index Entry: Maps path to CID
|
||||
type IndexEntry* = object
|
||||
path*: string ## Hierarchical path (e.g., "/agents/frankie/tasks")
|
||||
cid*: CID ## Content identifier
|
||||
timestamp*: int64 ## When indexed
|
||||
|
||||
## B-Tree Node (simplified for prototype)
|
||||
type BTreeNode* = object
|
||||
isLeaf*: bool
|
||||
keys*: seq[string] ## Paths
|
||||
values*: seq[CID] ## CIDs
|
||||
children*: seq[int] ## Child node indices (for internal nodes)
|
||||
|
||||
## L1 Index Handle
|
||||
type L1Index* = object
|
||||
entries*: Table[string, IndexEntry] ## Path -> Entry (simplified B-Tree)
|
||||
root*: string
|
||||
|
||||
proc initL1Index*(): L1Index =
|
||||
## Initialize empty L1 Index
|
||||
result.entries = initTable[string, IndexEntry]()
|
||||
result.root = "/"
|
||||
|
||||
## Insert or update path -> CID mapping
|
||||
proc insert*(index: var L1Index, path: string, cid: CID, timestamp: int64 = 0) =
|
||||
## Index a path to CID mapping
|
||||
index.entries[path] = IndexEntry(
|
||||
path: path,
|
||||
cid: cid,
|
||||
timestamp: if timestamp == 0: getTime().toUnix() else: timestamp
|
||||
)
|
||||
|
||||
## Lookup CID by exact path
|
||||
proc lookup*(index: L1Index, path: string): Option[CID] =
|
||||
## Find CID by exact path
|
||||
if index.entries.hasKey(path):
|
||||
return some(index.entries[path].cid)
|
||||
return none(CID)
|
||||
|
||||
## List all paths under a prefix (directory listing)
|
||||
proc list*(index: L1Index, prefix: string): seq[string] =
|
||||
## List all paths starting with prefix
|
||||
result = @[]
|
||||
for path in index.entries.keys:
|
||||
if path.startsWith(prefix):
|
||||
result.add(path)
|
||||
result.sort()
|
||||
|
||||
## Find paths matching glob pattern (simplified)
|
||||
proc glob*(index: L1Index, pattern: string): seq[string] =
|
||||
## Find paths matching pattern
|
||||
## Supports: * (any chars), ? (single char)
|
||||
result = @[]
|
||||
for path in index.entries.keys:
|
||||
# Simple glob matching (can be improved)
|
||||
if matchGlob(path, pattern):
|
||||
result.add(path)
|
||||
result.sort()
|
||||
|
||||
## Simple glob matcher
|
||||
proc matchGlob*(s, pattern: string): bool =
|
||||
## Match string against glob pattern
|
||||
var sIdx = 0
|
||||
var pIdx = 0
|
||||
|
||||
while pIdx < pattern.len:
|
||||
if pattern[pIdx] == '*':
|
||||
# Match any sequence
|
||||
if pIdx == pattern.len - 1:
|
||||
return true # * at end matches everything
|
||||
# Find next char after *
|
||||
let nextChar = pattern[pIdx + 1]
|
||||
while sIdx < s.len and s[sIdx] != nextChar:
|
||||
sIdx.inc
|
||||
pIdx += 2
|
||||
elif pattern[pIdx] == '?':
|
||||
# Match single char
|
||||
if sIdx >= s.len:
|
||||
return false
|
||||
sIdx.inc
|
||||
pIdx.inc
|
||||
else:
|
||||
# Match literal
|
||||
if sIdx >= s.len or s[sIdx] != pattern[pIdx]:
|
||||
return false
|
||||
sIdx.inc
|
||||
pIdx.inc
|
||||
|
||||
return sIdx == s.len
|
||||
|
||||
## Delete path from index
|
||||
proc remove*(index: var L1Index, path: string): bool =
|
||||
## Remove path from index
|
||||
if index.entries.hasKey(path):
|
||||
index.entries.del(path)
|
||||
return true
|
||||
return false
|
||||
|
||||
## Get all indexed paths
|
||||
proc paths*(index: L1Index): seq[string] =
|
||||
## Return all indexed paths (sorted)
|
||||
result = toSeq(index.entries.keys)
|
||||
result.sort()
|
||||
|
||||
## Export
|
||||
export L1Index, IndexEntry
|
||||
export initL1Index, insert, lookup, list, glob, remove, paths, matchGlob
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
## types.nim: Core Types for Nexus Context Protocol
|
||||
## RFC-NCP-001 Implementation
|
||||
## Author: Frankie (Silicon Architect)
|
||||
|
||||
import std/[tables, options, times]
|
||||
|
||||
## Content Identifier (CID) using Blake3
|
||||
## 256-bit hash for content-addressed storage
|
||||
type CID* = array[32, uint8]
|
||||
|
||||
## Content types for Context Nodes
|
||||
type ContentType* = enum
|
||||
ctText ## Plain text content
|
||||
ctImage ## Image data
|
||||
ctEmbedding ## Vector embedding (L2)
|
||||
ctToolCall ## Tool/function call
|
||||
ctMemory ## Agent memory
|
||||
ctSignature ## Cryptographic signature
|
||||
|
||||
## Context Node: The fundamental unit of NCP
|
||||
## Represents any piece of context in the system
|
||||
type ContextNode* = object
|
||||
cid*: CID ## Content identifier (Blake3 hash)
|
||||
parent*: Option[CID] ## Previous version (for versioning)
|
||||
path*: string ## Hierarchical path /agent/task/subtask
|
||||
contentType*: ContentType ## Type of content
|
||||
data*: seq[byte] ## Raw content bytes
|
||||
embedding*: Option[seq[float32]] ## Vector embedding (optional)
|
||||
timestamp*: int64 ## Unix nanoseconds
|
||||
metadata*: Table[string, string] ## Key-value metadata
|
||||
|
||||
## Path utilities for hierarchical addressing
|
||||
type Path* = object
|
||||
segments*: seq[string]
|
||||
absolute*: bool
|
||||
|
||||
proc initPath*(path: string): Path =
|
||||
## Parse a path string into segments
|
||||
## Example: "/agents/frankie/tasks" -> ["agents", "frankie", "tasks"]
|
||||
result.absolute = path.startsWith("/")
|
||||
result.segments = path.split("/").filterIt(it.len > 0)
|
||||
|
||||
proc toString*(p: Path): string =
|
||||
## Convert path back to string
|
||||
result = if p.absolute: "/" else: ""
|
||||
result.add(p.segments.join("/"))
|
||||
|
||||
## CID Generation (placeholder - actual Blake3 integration later)
|
||||
proc generateCID*(data: openArray[byte]): CID =
|
||||
## Generate content identifier from data
|
||||
## TODO: Integrate with actual Blake3 library
|
||||
## For now: simple XOR-based hash (NOT for production)
|
||||
var result: CID
|
||||
for i in 0..<32:
|
||||
result[i] = 0
|
||||
for i, b in data:
|
||||
result[i mod 32] = result[i mod 32] xor uint8(b)
|
||||
return result
|
||||
|
||||
## Context Node Operations
|
||||
proc initContextNode*(
|
||||
path: string,
|
||||
contentType: ContentType,
|
||||
data: openArray[byte]
|
||||
): ContextNode =
|
||||
## Initialize a new ContextNode
|
||||
result.path = path
|
||||
result.contentType = contentType
|
||||
result.data = @data
|
||||
result.cid = generateCID(data)
|
||||
result.timestamp = getTime().toUnix() * 1_000_000_000 # nanoseconds
|
||||
result.metadata = initTable[string, string]()
|
||||
|
||||
## Export utility functions
|
||||
export CID, ContentType, ContextNode, Path
|
||||
export initPath, toString, generateCID, initContextNode
|
||||
Loading…
Reference in New Issue