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
26 changes: 26 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Build

on:
pull_request:
branches: [ main ]
push:
branches: [ main ]

jobs:
build:
name: Build Solution
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Run build
run: |
chmod +x ./build.sh
./build.sh
36 changes: 36 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Test Coverage

on:
pull_request:
branches: [ main ]
push:
branches: [ main ]

jobs:
coverage:
name: Check Test Coverage
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Run coverage check
run: |
chmod +x ./coverage.sh
./coverage.sh 80
env:
MIN_COVERAGE: 80

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: TestResults/*/coverage.cobertura.xml
if-no-files-found: warn
24 changes: 23 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
see @docs/AGENTS.md

Ignore the "archived" directory.
Ignore the "archived" directory.

# Definition of done

- `format.sh` is passing without errors or warnings
- `build.sh` is passing without errors or warnings
- Test coverage is greater than 80% and `coverage.sh` is not failing
- Problems are not hidden, problems are addressed.

# C# Code Style

- Use .NET 10 and C# 14
- Always use `this.` prefix
- Keep magic values and constants in a centralized `Constants.cs` file
- One class per file, matching the class name with the file name
- Sort class methods by visibility: public first, private at the end
- Sort class fields and const by visibility: private, const, props
- Keep all fields and consts at the top of classes
- Ensure dirs and paths logic is cross-platform compatible
- Avoid generic/meaningless names like "Utils" "Common" "Lib"
- Use plural for Enum names, e.g. "EmbeddingsTypes"
- Always use explicit visibility
- Don't use primary constructors
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ The previous codebase remains in the repo for reference only.

Both versions exist purely as research projects used to explore new ideas and gather feedback from the community.

# What’s next

An important aspect of KM² is how we are building the next memory prototype. In parallel, our team is developing [Amplifier](https://github.com/microsoft/amplifier/tree/next), a platform for metacognitive AI engineering. We use Amplifier to build Amplifier itself — and in the same way, we are using Amplifier to build the next generation of Kernel Memory.

KM² will focus on the following areas, which will be documented in more detail when ready:
- quality of content generated
- privacy
- collaboration

## Disclaimer

> [!IMPORTANT] > **This is experimental software. _Expect things to break_.**
Expand Down
61 changes: 61 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash

set -e

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
cd $ROOT

echo "======================================="
echo " Building Kernel Memory Solution"
echo "======================================="
echo ""

# Clean previous build artifacts
echo "Cleaning previous build artifacts..."
dotnet clean --nologo --verbosity quiet
echo "✓ Clean complete"
echo ""

# Restore dependencies
echo "Restoring dependencies..."
dotnet restore --nologo
echo "✓ Restore complete"
echo ""

# Build solution with strict settings
echo "Building solution..."
echo ""

# Build with:
# - TreatWarningsAsErrors: Fail on any warnings (compliance requirement)
# - EnforceCodeStyleInBuild: Enforce code style during build
# - NoWarn: Empty (don't suppress any warnings)
dotnet build \
--no-restore \
--configuration Release \
/p:TreatWarningsAsErrors=true \
/p:EnforceCodeStyleInBuild=true \
/warnaserror

BUILD_RESULT=$?

echo ""

if [ $BUILD_RESULT -eq 0 ]; then
echo "======================================="
echo " ✅ Build Successful"
echo "======================================="
echo ""
echo "All projects built successfully with zero warnings."
exit 0
else
echo "======================================="
echo " ❌ Build Failed"
echo "======================================="
echo ""
echo "Build failed with errors or warnings."
echo "Review the output above for details."
echo ""
echo "Reminder: This project has zero-tolerance for warnings."
exit 1
fi
14 changes: 14 additions & 0 deletions clean.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash

set -e

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
cd $ROOT

rm -rf TestResults

rm -rf src/Core/bin
rm -rf src/Core/obj

rm -rf src/Main/bin
rm -rf src/Main/obj
61 changes: 61 additions & 0 deletions coverage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash

set -e

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
cd $ROOT

# Minimum coverage threshold - can be overridden via first argument
# Default: 80% (as specified in AGENTS.md)
MIN_COVERAGE=${1:-80}

echo "Running tests with coverage collection..."
echo ""

# Run tests with coverage using coverlet.collector
# --collect:"XPlat Code Coverage" enables the collector
# --results-directory specifies output location
dotnet test \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults

echo ""
echo "Coverage collection complete!"
echo ""

# Find the most recent coverage file (coverlet.collector creates it in a GUID subfolder)
COVERAGE_REPORT=$(find ./TestResults -name "coverage.cobertura.xml" | head -1)

echo "Coverage report location: $COVERAGE_REPORT"
echo ""

if [ -f "$COVERAGE_REPORT" ]; then
# Parse line coverage from cobertura XML
LINE_RATE=$(grep -o 'line-rate="[0-9.]*"' "$COVERAGE_REPORT" | head -1 | grep -o '[0-9.]*')

if [ -n "$LINE_RATE" ]; then
# Convert to percentage
COVERAGE_PCT=$(awk "BEGIN {printf \"%.2f\", $LINE_RATE * 100}")

echo "====================================="
echo " Test Coverage: ${COVERAGE_PCT}%"
echo " Threshold: ${MIN_COVERAGE}%"
echo "====================================="
echo ""

# Check if coverage meets threshold
MEETS_THRESHOLD=$(awk "BEGIN {print ($COVERAGE_PCT >= $MIN_COVERAGE) ? 1 : 0}")

if [ "$MEETS_THRESHOLD" -eq 0 ]; then
echo "❌ Coverage ${COVERAGE_PCT}% is below minimum threshold of ${MIN_COVERAGE}%"
exit 1
else
echo "✅ Coverage meets minimum threshold"
rm -rf TestResults
fi
else
echo "⚠️ Could not parse coverage percentage from report"
fi
else
echo "⚠️ Coverage report not found at: $COVERAGE_REPORT"
fi
3 changes: 3 additions & 0 deletions mem.slnx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<Solution>
<Folder Name="/tests/">
<Project Path="tests\Core.Tests\Core.Tests.csproj" Type="Classic C#" />
</Folder>
<Project Path="src\Core\Core.csproj" Type="Classic C#" />
<Project Path="src\Main\Main.csproj" Type="Classic C#" />
</Solution>
28 changes: 15 additions & 13 deletions src/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -378,23 +378,25 @@ dotnet_naming_rule.async_methods_end_in_async.severity = error
# Resharper #
#####################################

# disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
# ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
resharper_Condition_Is_Always_True_Or_False_According_To_Nullable_API_Contract_highlighting = none

# disable RedundantTypeArgumentsOfMethod
# RedundantTypeArgumentsOfMethod
resharper_Redundant_Type_Arguments_Of_Method_highlighting = none

# disable NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
# NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
resharper_Null_Coalescing_Condition_Is_Always_Not_Null_According_To_API_Contract_highlighting = none

# disable PartialTypeWithSinglePart
# PartialTypeWithSinglePart
resharper_Partial_Type_With_Single_Part_highlighting = none

# disable RedundantDefaultMemberInitializer
# RedundantDefaultMemberInitializer
resharper_Redundant_Default_Member_Initializer_highlighting = none

# disable ArrangeTypeModifiers
# ArrangeTypeModifiers
resharper_Arrange_Type_Modifiers_highlighting = none

# disable ArrangeTypeMemberModifiers
# ArrangeTypeMemberModifiers
resharper_Arrange_Type_Member_Modifiers_highlighting = none
# InconsistentNaming
resharper_Inconsistent_Naming_highlighting = none


# CA1056: URI properties should not be strings
# Suppressed for config classes where string URLs are more practical for JSON serialization
dotnet_diagnostic.CA1056.severity = none

79 changes: 79 additions & 0 deletions src/Core/Config/AppConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Text.Json.Serialization;
using KernelMemory.Core.Config.Cache;
using KernelMemory.Core.Config.Validation;

namespace KernelMemory.Core.Config;

/// <summary>
/// Root configuration for Kernel Memory application
/// Loaded from ~/.km/config.json or custom path
/// </summary>
public sealed class AppConfig : IValidatable
{
/// <summary>
/// Named memory nodes (e.g., "personal", "work")
/// Key is the node ID, value is the node configuration
/// </summary>
[JsonPropertyName("nodes")]
public Dictionary<string, NodeConfig> Nodes { get; set; } = new();

/// <summary>
/// Optional cache for embeddings to reduce API calls
/// </summary>
[JsonPropertyName("embeddingsCache")]
public CacheConfig? EmbeddingsCache { get; set; }

/// <summary>
/// Optional cache for LLM responses
/// </summary>
[JsonPropertyName("llmCache")]
public CacheConfig? LLMCache { get; set; }

/// <summary>
/// Validates the entire configuration tree
/// </summary>
/// <param name="path"></param>
public void Validate(string path = "")
{
if (this.Nodes.Count == 0)
{
throw new ConfigException("Nodes", "At least one node must be configured");
}

foreach (var (nodeId, nodeConfig) in this.Nodes)
{
if (string.IsNullOrWhiteSpace(nodeId))
{
throw new ConfigException("Nodes", "Node ID cannot be empty");
}

nodeConfig.Validate($"Nodes.{nodeId}");
}

this.EmbeddingsCache?.Validate("EmbeddingsCache");
this.LLMCache?.Validate("LLMCache");
}

/// <summary>
/// Creates a default configuration with a single "personal" node
/// using local SQLite storage
/// </summary>
public static AppConfig CreateDefault()
{
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var kmDir = Path.Combine(homeDir, ".km");
var personalNodeDir = Path.Combine(kmDir, "nodes", "personal");

return new AppConfig
{
Nodes = new Dictionary<string, NodeConfig>
{
["personal"] = NodeConfig.CreateDefaultPersonalNode(personalNodeDir)
},
EmbeddingsCache = CacheConfig.CreateDefaultSqliteCache(
Path.Combine(kmDir, "embeddings-cache.db")
),
LLMCache = null
};
}
}
Loading
Loading