Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
v v v v
+---------------------------------------------------------------------------------------------------+
| [ EXPORT / IMPORT (.yaadpack) (exportimport/pack.go) ] |
| - Portable JSON Export | - Signed Team Packs (.yaadpack) | - HMAC-SHA256 Signature Validator |
| - Portable JSON Export | - Shared Context Graph Projection | - Signed/HMAC Team Packs |
+-----------------------------------v---------------------------------------------------------------+
|
v
Expand All @@ -66,6 +66,17 @@ Storage Locations:

## Component Details

### Portable context projection (`portablegraph/`)

The portable projection consumes the `storage.Node` and `storage.Edge`
snapshots already selected by retrieval. It emits shared knowledge nodes,
portable relationships, provenance, temporal validity, and `observed` events
using `hawk-core-contracts/graph`.

It never runs retrieval, changes ranking, mutates SQLite, or exposes memory
content. Hawk records these projections as inference-context evidence and
links them to the owning execution session.

### 1. MCP Server (`internal/server/mcp.go`)

Implements Model Context Protocol over stdio transport.
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,18 @@ Not a flat list of memories. A directed graph with 8 edge types:
Enables: subgraph extraction, impact analysis, causal chain traversal.
</details>

<details>
<summary><b>Portable Context Graph</b></summary>

`portablegraph.Build` projects the exact nodes and edges selected by existing
Yaad retrieval into `hawk-core-contracts/graph`. The projection is
deterministic, repository-scoped, temporal, and metadata-only: memory content,
queries, source sessions, and agents are represented by SHA-256 digests.

Yaad's SQLite graph remains the source of truth; the portable graph is a
read-only handoff for hosts such as Hawk.
</details>

<details>
<summary><b>Intent-Aware 4-Path Search</b></summary>

Expand Down
113 changes: 113 additions & 0 deletions engine/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package engine

import (
"testing"
"time"
)

func TestNewQueryCache_Defaults(t *testing.T) {
t.Parallel()
c := NewQueryCache(0, 0)
if c.maxSize != 64 {
t.Errorf("default maxSize = %d, want 64", c.maxSize)
}
if c.ttl != 5*time.Minute {
t.Errorf("default ttl = %v, want 5m", c.ttl)
}
}

func TestQueryCache_GetPut(t *testing.T) {
t.Parallel()
c := NewQueryCache(10, time.Minute)

if got := c.Get("missing"); got != nil {
t.Errorf("Get(missing) = %v, want nil", got)
}

result := &RecallResult{Nodes: nil}
c.Put("q1", result)

if got := c.Get("q1"); got != result {
t.Errorf("Get(q1) = %v, want original result", got)
}
}

func TestQueryCache_Invalidate(t *testing.T) {
t.Parallel()
c := NewQueryCache(10, time.Minute)
c.Put("q1", &RecallResult{})

c.Invalidate()

if got := c.Get("q1"); got != nil {
t.Errorf("Get after Invalidate = %v, want nil", got)
}
}

func TestQueryCache_TTLExpiry(t *testing.T) {
t.Parallel()
c := NewQueryCache(10, time.Millisecond)
c.Put("q1", &RecallResult{})

time.Sleep(5 * time.Millisecond)

if got := c.Get("q1"); got != nil {
t.Errorf("Get after TTL = %v, want nil (expired)", got)
}
}

func TestQueryCache_Eviction(t *testing.T) {
t.Parallel()
c := NewQueryCache(2, time.Minute)
c.Put("a", &RecallResult{})
c.Put("b", &RecallResult{})
c.Put("c", &RecallResult{}) // evicts "a"

if got := c.Get("a"); got != nil {
t.Errorf("Get(a) after eviction = %v, want nil", got)
}
if got := c.Get("b"); got == nil {
t.Error("Get(b) = nil, want present")
}
if got := c.Get("c"); got == nil {
t.Error("Get(c) = nil, want present")
}
size, _ := c.Stats()
if size != 2 {
t.Errorf("size = %d, want 2", size)
}
}

func TestQueryCache_StatsAndClear(t *testing.T) {
t.Parallel()
c := NewQueryCache(10, time.Minute)
c.Put("q1", &RecallResult{})
c.Put("q2", &RecallResult{})

size, version := c.Stats()
if size != 2 {
t.Errorf("size = %d, want 2", size)
}
if version != 0 {
t.Errorf("version = %d, want 0", version)
}

c.Clear()
size, _ = c.Stats()
if size != 0 {
t.Errorf("size after Clear = %d, want 0", size)
}
}

func TestCacheKey(t *testing.T) {
t.Parallel()
key := CacheKey(RecallOpts{Query: "hello", Type: "convention", Project: "myproj", Limit: 10, Depth: 2})
if key == "" {
t.Error("CacheKey returned empty string")
}
// Different opts must yield different keys.
other := CacheKey(RecallOpts{Query: "world", Type: "convention", Project: "myproj", Limit: 10, Depth: 2})
if key == other {
t.Errorf("different queries produced same key %q", key)
}
}
28 changes: 28 additions & 0 deletions engine/context_pack.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package engine

import (
"fmt"
"regexp"
"sort"
"strings"

Expand Down Expand Up @@ -179,3 +180,30 @@ type SummarizedMemory struct {
Content string
Type string
}

var (
classifyConventionRe = regexp.MustCompile(`(?i)(always|never|must|should|prefer|avoid|use .+ instead|don't|do not|convention|rule|standard)`)
classifyDecisionRe = regexp.MustCompile(`(?i)(decided|chose|picked|selected|went with|switched to|migrated|adopted|we use)`)
classifyBugRe = regexp.MustCompile(`(?i)(bug|fix|issue|error|broken|crash|race condition|deadlock|leak|regression)`)
classifyTaskRe = regexp.MustCompile(`(?i)(todo|TODO|FIXME|HACK|in progress|need to|should implement|blocked)`)
classifyPrefRe = regexp.MustCompile(`(?i)(i prefer|i like|my style|i want|personally|i always)`)
)

// classifyContent maps a line of conversation to a memory node type based on
// keyword heuristics. Returns "" when no type matches.
func classifyContent(text string) string {
switch {
case classifyDecisionRe.MatchString(text):
return "decision"
case classifyConventionRe.MatchString(text):
return "convention"
case classifyBugRe.MatchString(text):
return "bug"
case classifyTaskRe.MatchString(text):
return "task"
case classifyPrefRe.MatchString(text):
return "preference"
default:
return ""
}
}
Loading
Loading