chore: add schemas and just recipes
Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
Nickel Type Check / Nickel Type Checking (push) Has been cancelled

This commit is contained in:
Jesús Pérez 2026-01-23 16:12:50 +00:00
parent 2b4d548aad
commit 9ea04852a8
Signed by: jesus
GPG Key ID: 9F243E355E0BC939
43 changed files with 5452 additions and 0 deletions

94
justfile Normal file
View File

@ -0,0 +1,94 @@
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ Knowledge Base - Justfile ║
# ║ Modular workspace orchestration ║
# ║ Features: CLI, MCP, Nickel, NuShell Scripts ║
# ╚══════════════════════════════════════════════════════════════════════╝
# Import feature-specific modules
mod build "justfiles/build.just" # Build recipes (kogral-core, kogral-cli, kogral-mcp)
mod test "justfiles/test.just" # Test suite (unit, integration, docs)
mod dev "justfiles/dev.just" # Development tools (fmt, lint, watch, docs)
mod ci "justfiles/ci.just" # CI/CD pipeline (validate, test, build)
mod nickel "justfiles/nickel.just" # Nickel integration (schema validation, export)
mod docs "justfiles/docs.just" # Documentation (mdBook build, serve)
mod scripts "justfiles/scripts.just" # NuShell scripts management
# === ORCHESTRATION RECIPES ===
# Default: show available commands
default:
@just --list
# Full development workflow
[doc("Run check + fmt + lint + test")]
check-all:
@just dev::fmt-check
@just dev::lint-all
@just test::all
# Full CI workflow (format + lint + test + build all variants)
[doc("Complete CI pipeline: format + lint + test + build")]
ci-full:
@just ci::check
@just ci::test-all
@just build::all
# Quick start development environment
[doc("Quick dev setup: build default + start watching")]
dev-start:
@just dev::build
@just dev::watch
# Build documentation and serve locally
[doc("Build and serve mdBook documentation")]
docs-serve:
@just docs::serve
# Validate Nickel schemas
[doc("Validate all Nickel configuration schemas")]
validate-schemas:
@just nickel::validate-all
# === MODULAR HELP ===
# Show help by module: just help build, just help test, etc
[doc("Show help for a specific module")]
help MODULE="":
@if [ -z "{{ MODULE }}" ]; then \
echo "KNOWLEDGE BASE - MODULAR JUSTFILE HELP"; \
echo ""; \
echo "Available modules:"; \
echo " just help build Build commands"; \
echo " just help test Test commands"; \
echo " just help dev Development utilities"; \
echo " just help ci CI/CD pipeline"; \
echo " just help nickel Nickel schema tools"; \
echo " just help docs Documentation tools"; \
echo " just help scripts NuShell scripts"; \
echo ""; \
echo "Orchestration:"; \
echo " just check-all Format check + lint + test"; \
echo " just ci-full Full CI pipeline"; \
echo " just dev-start Quick dev setup"; \
echo " just docs-serve Build and serve docs"; \
echo " just validate-schemas Validate Nickel schemas"; \
echo ""; \
echo "Use: just help <module> for details"; \
elif [ "{{ MODULE }}" = "build" ]; then \
just build::help; \
elif [ "{{ MODULE }}" = "test" ]; then \
just test::help; \
elif [ "{{ MODULE }}" = "dev" ]; then \
just dev::help; \
elif [ "{{ MODULE }}" = "ci" ]; then \
just ci::help; \
elif [ "{{ MODULE }}" = "nickel" ]; then \
just nickel::help; \
elif [ "{{ MODULE }}" = "docs" ]; then \
just docs::help; \
elif [ "{{ MODULE }}" = "scripts" ]; then \
just scripts::help; \
else \
echo "Unknown module: {{ MODULE }}"; \
echo "Available: build, test, dev, ci, nickel, docs, scripts"; \
fi

View File

View File

View File

0
justfiles/49969output Normal file
View File

121
justfiles/build.just Normal file
View File

@ -0,0 +1,121 @@
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ BUILD RECIPES ║
# ║ Build workspace with different feature flags ║
# ╚══════════════════════════════════════════════════════════════════════╝
# === FEATURE FLAGS ===
FEATURES_CORE_DEFAULT := "filesystem"
FEATURES_CORE_FULL := "filesystem,surrealdb,fastembed,full"
FEATURES_CORE_STORAGE := "filesystem,surrealdb"
FEATURES_CORE_EMBEDDINGS := "filesystem,fastembed"
# Help for build module
help:
@echo "BUILD MODULE"
@echo ""
@echo "Build default (filesystem backend):"
@echo " just build::default"
@echo ""
@echo "Build specific components:"
@echo " just build::core Build kogral-core library"
@echo " just build::cli Build kogral-cli binary"
@echo " just build::mcp Build kogral-mcp server"
@echo ""
@echo "Build with features:"
@echo " just build::core-full Build kogral-core with all features"
@echo " just build::core-db Build kogral-core with SurrealDB"
@echo " just build::core-ai Build kogral-core with fastembed"
@echo ""
@echo "Build combined:"
@echo " just build::all Build all crates"
@echo " just build::workspace Build entire workspace"
@echo ""
@echo "Build release:"
@echo " just build::release Release build (all features)"
@echo ""
@echo "Check compilation:"
@echo " just build::check Check without building"
# === DEFAULT BUILD ===
# Build workspace with default features
[doc("Build default: filesystem backend only")]
default:
@echo "=== Building default features ==="
cargo build --workspace
# === COMPONENT BUILDS ===
# Build kogral-core library
[doc("Build kogral-core library (default features)")]
core:
@echo "=== Building kogral-core ==="
cargo build --package kogral-core
# Build kogral-cli binary
[doc("Build kogral-cli command-line tool")]
cli:
@echo "=== Building kogral-cli ==="
cargo build --package kogral-cli
# Build kogral-mcp server
[doc("Build kogral-mcp MCP server")]
mcp:
@echo "=== Building kogral-mcp ==="
cargo build --package kogral-mcp
# === FEATURE-SPECIFIC BUILDS ===
# Build kogral-core with all features
[doc("Build kogral-core with all features (filesystem, surrealdb, fastembed)")]
core-full:
@echo "=== Building kogral-core (all features) ==="
cargo build --package kogral-core --features {{ FEATURES_CORE_FULL }}
# Build kogral-core with SurrealDB backend
[doc("Build kogral-core with SurrealDB support")]
core-db:
@echo "=== Building kogral-core (SurrealDB) ==="
cargo build --package kogral-core --features {{ FEATURES_CORE_STORAGE }}
# Build kogral-core with fastembed
[doc("Build kogral-core with local embeddings (fastembed)")]
core-ai:
@echo "=== Building kogral-core (fastembed) ==="
cargo build --package kogral-core --features {{ FEATURES_CORE_EMBEDDINGS }}
# === COMBINED BUILDS ===
# Build all crates
[doc("Build all crates (kogral-core, kogral-cli, kogral-mcp)")]
all:
@echo "=== Building all crates ==="
@just build::core
@just build::cli
@just build::mcp
@echo "✓ All crates built"
# Build entire workspace
[doc("Build entire workspace with all features")]
workspace:
@echo "=== Building workspace (all features) ==="
cargo build --workspace --all-features
# === RELEASE BUILD ===
# Build release version with all features
[doc("Build release version (optimized, all features)")]
release:
@echo "=== Building release (all features) ==="
cargo build --workspace --all-features --release
@echo "✓ Release build complete"
@ls -lh target/release/kogral-cli target/release/kogral-mcp 2>/dev/null || true
# === VALIDATION ===
# Check compilation without building
[doc("Check code compiles without building artifacts")]
check:
@echo "=== Checking compilation ==="
cargo check --workspace --all-features
@echo "✓ Compilation check passed"

83
justfiles/ci.just Normal file
View File

@ -0,0 +1,83 @@
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ CI/CD PIPELINE ║
# ║ Validation and testing for CI ║
# ╚══════════════════════════════════════════════════════════════════════╝
# Help for CI module
help:
@echo "CI/CD MODULE"
@echo ""
@echo "Full pipeline:"
@echo " just ci::pipeline Run complete CI pipeline"
@echo " just ci::quick Quick CI check (fmt + lint + test)"
@echo ""
@echo "Individual checks:"
@echo " just ci::check Format + lint check"
@echo " just ci::test-all Run all tests"
@echo " just ci::build-all Build all features"
@echo " just ci::validate Validate Nickel schemas"
@echo ""
@echo "Release checks:"
@echo " just ci::release-check Validate release build"
# === FULL PIPELINE ===
# Run complete CI pipeline
[doc("Complete CI pipeline: format + lint + test + build + validate")]
pipeline:
@echo "=== CI Pipeline ==="
@just ci::check
@just ci::validate
@just ci::test-all
@just ci::build-all
@echo "✓ CI pipeline completed successfully"
# Quick CI check (for fast feedback)
[doc("Quick CI: format + lint + test (default features)")]
quick:
@echo "=== Quick CI Check ==="
@just ci::check
@just test::all
@echo "✓ Quick CI passed"
# === INDIVIDUAL CHECKS ===
# Format and lint check
[doc("Check code format and run linter")]
check:
@echo "=== Checking format and linting ==="
cargo fmt --all -- --check
cargo clippy --workspace --all-features -- -D warnings
@echo "✓ Format and lint check passed"
# Run all tests (all features)
[doc("Run all tests with all features")]
test-all:
@echo "=== Running all tests ==="
cargo test --workspace --all-features
@echo "✓ All tests passed"
# Build all feature combinations
[doc("Build all feature combinations")]
build-all:
@echo "=== Building all features ==="
cargo build --workspace
cargo build --workspace --all-features
@echo "✓ All builds successful"
# Validate Nickel schemas
[doc("Validate all Nickel configuration schemas")]
validate:
@echo "=== Validating Nickel schemas ==="
@just nickel::validate-all
@echo "✓ Nickel validation passed"
# === RELEASE CHECKS ===
# Validate release build
[doc("Validate release build (optimized)")]
release-check:
@echo "=== Validating release build ==="
cargo build --workspace --all-features --release
cargo test --workspace --all-features --release
@echo "✓ Release build validated"

123
justfiles/dev.just Normal file
View File

@ -0,0 +1,123 @@
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ DEVELOPMENT UTILITIES ║
# ║ Watch, format, lint, docs ║
# ╚══════════════════════════════════════════════════════════════════════╝
# Help for dev module
help:
@echo "DEVELOPMENT MODULE"
@echo ""
@echo "Code quality:"
@echo " just dev::fmt Format code (Rust + TOML + Nickel)"
@echo " just dev::fmt-check Check format (no changes)"
@echo " just dev::lint Run clippy linter"
@echo " just dev::lint-all Lint Rust + Nickel + NuShell"
@echo " just dev::audit Audit dependencies"
@echo ""
@echo "Documentation:"
@echo " just dev::docs Generate and open docs"
@echo " just dev::docs-gen Generate docs only"
@echo ""
@echo "Common tasks:"
@echo " just dev::build Build default features"
@echo " just dev::watch Watch and rebuild on changes"
@echo " just dev::check Check + fmt + lint"
@echo ""
@echo "Inspect:"
@echo " just dev::info Show workspace info"
@echo " just dev::tree Show dependency tree"
# === CODE FORMATTING ===
# Format all code (Rust + TOML + Nickel)
[doc("Format Rust, TOML, and Nickel code")]
fmt:
@echo "=== Formatting code ==="
cargo fmt --all
@echo "✓ Rust code formatted"
# Check code formatting without modifying
[doc("Check code format (no changes)")]
fmt-check:
@echo "=== Checking code format ==="
cargo fmt --all -- --check
@echo "✓ Code format check passed"
# === LINTING ===
# Run clippy linter on Rust code
[doc("Run clippy linter")]
lint:
@echo "=== Running clippy ==="
cargo clippy --workspace --all-features -- -D warnings
@echo "✓ Clippy check passed"
# Lint all languages (Rust + Nickel + NuShell)
[doc("Lint Rust, Nickel, and NuShell code")]
lint-all:
@echo "=== Linting all code ==="
@just dev::lint
@just nickel::lint
@echo "✓ All linting passed"
# Audit dependencies for security vulnerabilities
[doc("Audit dependencies")]
audit:
@echo "=== Auditing dependencies ==="
cargo audit
@echo "✓ Dependency audit passed"
# === DOCUMENTATION ===
# Generate and open documentation
[doc("Generate and open Rust docs")]
docs:
@echo "=== Generating documentation ==="
cargo doc --workspace --all-features --no-deps --open
# Generate documentation only (no open)
[doc("Generate Rust docs only")]
docs-gen:
@echo "=== Generating documentation ==="
cargo doc --workspace --all-features --no-deps
# === COMMON TASKS ===
# Build default features
[doc("Build workspace with default features")]
build:
@cargo build --workspace
# Watch and rebuild on changes
[doc("Watch for changes and rebuild")]
watch:
@echo "=== Watching for changes ==="
cargo watch -x "build --workspace"
# Check + format + lint
[doc("Run check + fmt + lint")]
check:
@just dev::fmt-check
@just dev::lint
@cargo check --workspace --all-features
@echo "✓ All checks passed"
# === INSPECT ===
# Show workspace information
[doc("Show workspace information")]
info:
@echo "=== Workspace Information ==="
@cargo tree --workspace --depth 1
@echo ""
@echo "=== Crates ==="
@ls -1 crates/
@echo ""
@echo "=== Features (kogral-core) ==="
@cargo tree --package kogral-core --features full --depth 0
# Show dependency tree
[doc("Show complete dependency tree")]
tree:
@echo "=== Dependency Tree ==="
cargo tree --workspace

99
justfiles/docs.just Normal file
View File

@ -0,0 +1,99 @@
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ DOCUMENTATION TOOLS ║
# ║ mdBook build, serve, and management ║
# ╚══════════════════════════════════════════════════════════════════════╝
# === PATHS ===
DOCS_DIR := "docs"
BOOK_DIR := "book"
# Help for docs module
help:
@echo "DOCUMENTATION MODULE"
@echo ""
@echo "Build and serve:"
@echo " just docs::build Build mdBook"
@echo " just docs::serve Serve mdBook locally"
@echo " just docs::watch Watch and rebuild on changes"
@echo ""
@echo "Validation:"
@echo " just docs::test Test documentation examples"
@echo " just docs::check-links Check for broken links"
@echo ""
@echo "Cleanup:"
@echo " just docs::clean Clean build artifacts"
# === BUILD AND SERVE ===
# Build mdBook documentation
[doc("Build mdBook documentation")]
build:
@echo "=== Building mdBook ==="
@if command -v mdbook >/dev/null 2>&1; then \
cd {{ DOCS_DIR }} && mdbook build; \
echo "✓ mdBook built successfully"; \
else \
echo "Error: mdbook not installed"; \
echo "Install with: cargo install mdbook"; \
exit 1; \
fi
# Serve mdBook locally
[doc("Serve mdBook locally (http://localhost:3000)")]
serve:
@echo "=== Serving mdBook ==="
@if command -v mdbook >/dev/null 2>&1; then \
echo "Opening http://localhost:3000"; \
cd {{ DOCS_DIR }} && mdbook serve --open; \
else \
echo "Error: mdbook not installed"; \
echo "Install with: cargo install mdbook"; \
exit 1; \
fi
# Watch and rebuild on changes
[doc("Watch docs and rebuild on changes")]
watch:
@echo "=== Watching documentation ==="
@if command -v mdbook >/dev/null 2>&1; then \
cd {{ DOCS_DIR }} && mdbook watch; \
else \
echo "Error: mdbook not installed"; \
echo "Install with: cargo install mdbook"; \
exit 1; \
fi
# === VALIDATION ===
# Test documentation code examples
[doc("Test documentation code examples")]
test:
@echo "=== Testing documentation examples ==="
@if command -v mdbook >/dev/null 2>&1; then \
cd {{ DOCS_DIR }} && mdbook test; \
echo "✓ Documentation tests passed"; \
else \
echo "Error: mdbook not installed"; \
exit 1; \
fi
# Check for broken links
[doc("Check for broken links in documentation")]
check-links:
@echo "=== Checking for broken links ==="
@if command -v mdbook-linkcheck >/dev/null 2>&1; then \
cd {{ DOCS_DIR }} && mdbook-linkcheck; \
echo "✓ No broken links found"; \
else \
echo "Warning: mdbook-linkcheck not installed"; \
echo "Install with: cargo install mdbook-linkcheck"; \
fi
# === CLEANUP ===
# Clean build artifacts
[doc("Clean mdBook build artifacts")]
clean:
@echo "=== Cleaning mdBook artifacts ==="
@rm -rf {{ DOCS_DIR }}/{{ BOOK_DIR }}
@echo "✓ Cleaned build artifacts"

184
justfiles/nickel.just Normal file
View File

@ -0,0 +1,184 @@
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ NICKEL INTEGRATION ║
# ║ Schema validation and configuration export ║
# ╚══════════════════════════════════════════════════════════════════════╝
# === PATHS ===
SCHEMA_DIR := "schemas"
CONFIG_DIR := "config"
TYPEDIALOG_DIR := ".typedialog/kogral"
TYPEDIALOG_CORE := ".typedialog/kogral/core"
TYPEDIALOG_MODES := ".typedialog/kogral/modes"
TYPEDIALOG_SCRIPTS := ".typedialog/kogral/scripts"
# Help for Nickel module
help:
@echo "NICKEL MODULE"
@echo ""
@echo "Validation:"
@echo " just nickel::validate-all Validate all schemas (legacy)"
@echo " just nickel::validate-config Validate TypeDialog config schemas"
@echo " just nickel::validate FILE Validate specific schema"
@echo " just nickel::typecheck FILE Typecheck Nickel file"
@echo ""
@echo "Export:"
@echo " just nickel::export FILE Export config to JSON"
@echo " just nickel::export-all Export all example configs (legacy)"
@echo " just nickel::export-modes Export all TypeDialog modes to JSON"
@echo " just nickel::export-dev Export dev mode configuration"
@echo " just nickel::export-prod Export prod mode configuration"
@echo " just nickel::export-test Export test mode configuration"
@echo ""
@echo "Sync & Generate:"
@echo " just nickel::sync-to-schemas Sync .typedialog/kogral → schemas/kogral"
@echo " just nickel::generate-config Generate .kogral/config.json (default: dev mode)"
@echo ""
@echo "Linting:"
@echo " just nickel::lint Lint Nickel schemas"
@echo " just nickel::fmt FILE Format Nickel file"
@echo ""
@echo "Testing:"
@echo " just nickel::test Test schema examples"
# === VALIDATION ===
# Validate all Nickel schemas
[doc("Validate all Nickel schemas")]
validate-all:
@echo "=== Validating Nickel schemas ==="
@for file in {{ SCHEMA_DIR }}/*.ncl; do \
echo "Validating $$file..."; \
nickel typecheck "$$file" || exit 1; \
done
@echo "✓ All schemas valid"
# Validate specific Nickel file
[doc("Validate specific Nickel file")]
validate FILE:
@echo "=== Validating {{ FILE }} ==="
nickel typecheck "{{ FILE }}"
@echo "✓ {{ FILE }} is valid"
# Typecheck Nickel file
[doc("Typecheck Nickel file")]
typecheck FILE:
@nickel typecheck "{{ FILE }}"
# === EXPORT ===
# Export config file to JSON
[doc("Export Nickel config to JSON")]
export FILE:
@echo "=== Exporting {{ FILE }} to JSON ==="
@nickel export --format json "{{ FILE }}"
# Export all example configs
[doc("Export all example configs to JSON")]
export-all:
@echo "=== Exporting all configs ==="
@for file in {{ CONFIG_DIR }}/*.ncl; do \
output="$${file%.ncl}.json"; \
echo "Exporting $$file → $$output"; \
nickel export --format json "$$file" > "$$output"; \
done
@echo "✓ All configs exported"
# === LINTING ===
# Lint Nickel schemas (check formatting and style)
[doc("Lint Nickel schemas")]
lint:
@echo "=== Linting Nickel files ==="
@for file in {{ SCHEMA_DIR }}/*.ncl {{ CONFIG_DIR }}/*.ncl; do \
echo "Checking $$file..."; \
nickel typecheck "$$file" || exit 1; \
done
@echo "✓ Nickel lint passed"
# Format Nickel file
[doc("Format Nickel file")]
fmt FILE:
@echo "=== Formatting {{ FILE }} ==="
nickel format "{{ FILE }}"
# === TYPEDIALOG CONFIGURATION ===
# Validate TypeDialog config schemas
[doc("Validate TypeDialog config schemas (.typedialog/kogral/)")]
validate-config:
@echo "=== Validating TypeDialog config schemas ==="
nu {{justfile_directory()}}/.typedialog/kogral/scripts/validate-config.nu
# Export all TypeDialog modes to JSON
[doc("Export all TypeDialog modes to JSON")]
export-modes: export-dev export-prod export-test
@echo "✓ All modes exported"
# Export dev mode configuration
[doc("Export dev mode configuration")]
export-dev:
@mkdir -p .kogral && \
nickel export --format json .typedialog/kogral/modes/dev.ncl > .kogral/config.dev.json && \
echo " ✓ Exported: .kogral/config.dev.json"
# Export prod mode configuration
[doc("Export prod mode configuration")]
export-prod:
@mkdir -p .kogral && \
nickel export --format json .typedialog/kogral/modes/prod.ncl > .kogral/config.prod.json && \
echo " ✓ Exported: .kogral/config.prod.json"
# Export test mode configuration
[doc("Export test mode configuration")]
export-test:
@mkdir -p .kogral && \
nickel export --format json .typedialog/kogral/modes/test.ncl > .kogral/config.test.json && \
echo " ✓ Exported: .kogral/config.test.json"
# Sync .typedialog/kogral to schemas/kogral (source of truth)
[doc("Sync .typedialog/kogral → schemas/kogral")]
sync-to-schemas:
@echo "=== Syncing TypeDialog config to schemas ==="
@mkdir -p schemas/kogral
@echo " Copying core schemas..."
@cp {{ TYPEDIALOG_CORE }}/contracts.ncl schemas/kogral/
@cp {{ TYPEDIALOG_CORE }}/defaults.ncl schemas/kogral/
@cp {{ TYPEDIALOG_CORE }}/helpers.ncl schemas/kogral/
@mkdir -p schemas/kogral/modes
@echo " Copying mode overlays..."
@cp {{ TYPEDIALOG_MODES }}/dev.ncl schemas/kogral/modes/
@cp {{ TYPEDIALOG_MODES }}/prod.ncl schemas/kogral/modes/
@cp {{ TYPEDIALOG_MODES }}/test.ncl schemas/kogral/modes/
@echo "✓ Sync completed"
# Generate .kogral/config.json for CLI/MCP (default: dev mode)
[doc("Generate .kogral/config.json (default: dev mode)")]
generate-config MODE="dev":
@echo "=== Generating config from {{ MODE }} mode ==="
@if [ "{{ MODE }}" != "dev" ] && [ "{{ MODE }}" != "prod" ] && [ "{{ MODE }}" != "test" ]; then \
echo "Error: Invalid mode. Must be dev, prod, or test"; \
exit 1; \
fi
@mkdir -p .kogral
nu {{justfile_directory()}}/.typedialog/kogral/scripts/generate-configs.nu --mode {{ MODE }} --output .kogral
@echo "✓ Configuration generated at .kogral/config.json"
# === TESTING ===
# Test schema examples
[doc("Test schema examples")]
test:
@echo "=== Testing Nickel schemas ==="
@just nickel::validate-all
@just nickel::export-all
@echo "✓ Schema tests passed"
# Test TypeDialog configuration pipeline
[doc("Test TypeDialog configuration pipeline")]
test-config:
@echo "=== Testing TypeDialog config pipeline ==="
@just nickel::validate-config
@just nickel::export-modes
@just nickel::sync-to-schemas
@just nickel::generate-config dev
@echo "✓ TypeDialog config pipeline tests passed"

137
justfiles/scripts.just Normal file
View File

@ -0,0 +1,137 @@
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ NUSHELL SCRIPTS ║
# ║ Maintenance and automation scripts ║
# ╚══════════════════════════════════════════════════════════════════════╝
# === PATHS ===
SCRIPTS_DIR := "scripts"
# Help for scripts module
help:
@echo "NUSHELL SCRIPTS MODULE"
@echo ""
@echo "Sync and backup:"
@echo " just scripts::sync Sync filesystem with SurrealDB"
@echo " just scripts::backup Backup knowledge base"
@echo " just scripts::reindex Rebuild embeddings index"
@echo ""
@echo "Import/Export:"
@echo " just scripts::import-logseq DIR Import from Logseq graph"
@echo " just scripts::export-logseq DIR Export to Logseq format"
@echo ""
@echo "Statistics:"
@echo " just scripts::stats Show KOGRAL statistics"
@echo " just scripts::stats-json Show stats in JSON format"
@echo ""
@echo "Maintenance:"
@echo " just scripts::migrate Run schema migrations"
@echo " just scripts::check-scripts Validate NuShell scripts"
# === SYNC AND BACKUP ===
# Sync filesystem with SurrealDB
[doc("Sync filesystem with SurrealDB storage")]
sync DIRECTION="bidirectional":
@echo "=== Syncing KOGRAL ({{ DIRECTION }}) ==="
@if command -v nu >/dev/null 2>&1; then \
nu {{ SCRIPTS_DIR }}/kogral-sync.nu --direction {{ DIRECTION }}; \
else \
echo "Error: nushell not installed"; \
exit 1; \
fi
# Backup knowledge base
[doc("Backup knowledge base to archive")]
backup:
@echo "=== Backing up KOGRAL ==="
@if command -v nu >/dev/null 2>&1; then \
nu {{ SCRIPTS_DIR }}/kogral-backup.nu --format tar --compress; \
else \
echo "Error: nushell not installed"; \
exit 1; \
fi
# Rebuild embeddings index
[doc("Rebuild embeddings index")]
reindex PROVIDER="fastembed":
@echo "=== Reindexing embeddings ({{ PROVIDER }}) ==="
@if command -v nu >/dev/null 2>&1; then \
nu {{ SCRIPTS_DIR }}/kogral-reindex.nu --provider {{ PROVIDER }}; \
else \
echo "Error: nushell not installed"; \
exit 1; \
fi
# === IMPORT/EXPORT ===
# Import from Logseq graph
[doc("Import from Logseq graph directory")]
import-logseq DIR:
@echo "=== Importing from Logseq: {{ DIR }} ==="
@if command -v nu >/dev/null 2>&1; then \
nu {{ SCRIPTS_DIR }}/kogral-import-logseq.nu "{{ DIR }}"; \
else \
echo "Error: nushell not installed"; \
exit 1; \
fi
# Export to Logseq format
[doc("Export to Logseq format")]
export-logseq DIR:
@echo "=== Exporting to Logseq: {{ DIR }} ==="
@if command -v nu >/dev/null 2>&1; then \
nu {{ SCRIPTS_DIR }}/kogral-export-logseq.nu "{{ DIR }}"; \
else \
echo "Error: nushell not installed"; \
exit 1; \
fi
# === STATISTICS ===
# Show KOGRAL statistics (summary format)
[doc("Show knowledge base statistics")]
stats:
@if command -v nu >/dev/null 2>&1; then \
nu {{ SCRIPTS_DIR }}/kogral-stats.nu --format summary --show-tags; \
else \
echo "Error: nushell not installed"; \
exit 1; \
fi
# Show KOGRAL statistics in JSON format
[doc("Show knowledge base statistics (JSON)")]
stats-json:
@if command -v nu >/dev/null 2>&1; then \
nu {{ SCRIPTS_DIR }}/kogral-stats.nu --format json; \
else \
echo "Error: nushell not installed"; \
exit 1; \
fi
# === MAINTENANCE ===
# Run schema migrations
[doc("Run database schema migrations")]
migrate TARGET="latest":
@echo "=== Running migrations (target: {{ TARGET }}) ==="
@if command -v nu >/dev/null 2>&1; then \
nu {{ SCRIPTS_DIR }}/kogral-migrate.nu --target {{ TARGET }}; \
else \
echo "Error: nushell not installed"; \
exit 1; \
fi
# Validate NuShell scripts syntax
[doc("Validate NuShell scripts")]
check-scripts:
@echo "=== Validating NuShell scripts ==="
@if command -v nu >/dev/null 2>&1; then \
for script in {{ SCRIPTS_DIR }}/*.nu; do \
echo "Checking $$script..."; \
nu -c "source $$script; help" >/dev/null 2>&1 || { echo "✗ $$script has syntax errors"; exit 1; }; \
done; \
echo "✓ All scripts valid"; \
else \
echo "Error: nushell not installed"; \
exit 1; \
fi

89
justfiles/test.just Normal file
View File

@ -0,0 +1,89 @@
# ╔══════════════════════════════════════════════════════════════════════╗
# ║ TEST RECIPES ║
# ║ Test workspace with different feature flags ║
# ╚══════════════════════════════════════════════════════════════════════╝
# Help for test module
help:
@echo "TEST MODULE"
@echo ""
@echo "Run all tests:"
@echo " just test::all All tests (default features)"
@echo " just test::all-features All tests with all features"
@echo ""
@echo "Test specific crates:"
@echo " just test::core Test kogral-core library"
@echo " just test::cli Test kogral-cli binary"
@echo " just test::mcp Test kogral-mcp server"
@echo ""
@echo "Test specific features:"
@echo " just test::core-db Test SurrealDB backend"
@echo " just test::core-ai Test fastembed integration"
@echo ""
@echo "Integration & docs:"
@echo " just test::integration Run integration tests only"
@echo " just test::doc Test documentation examples"
# === FULL TEST SUITES ===
# Run all tests (default features)
[doc("Run all tests with default features")]
all:
@echo "=== Testing workspace (default features) ==="
cargo test --workspace --lib
@echo "✓ All tests passed"
# Run all tests with all features enabled
[doc("Run all tests with all features")]
all-features:
@echo "=== Testing workspace (all features) ==="
cargo test --workspace --all-features
@echo "✓ All tests passed (all features)"
# === CRATE-SPECIFIC TESTS ===
# Test kogral-core library
[doc("Test kogral-core library")]
core:
@echo "=== Testing kogral-core ==="
cargo test --package kogral-core --lib
# Test kogral-cli binary
[doc("Test kogral-cli binary")]
cli:
@echo "=== Testing kogral-cli ==="
cargo test --package kogral-cli
# Test kogral-mcp server
[doc("Test kogral-mcp server")]
mcp:
@echo "=== Testing kogral-mcp ==="
cargo test --package kogral-mcp --lib
# === FEATURE-SPECIFIC TESTS ===
# Test SurrealDB backend
[doc("Test kogral-core with SurrealDB backend")]
core-db:
@echo "=== Testing kogral-core (SurrealDB) ==="
cargo test --package kogral-core --features surrealdb
# Test fastembed integration
[doc("Test kogral-core with fastembed")]
core-ai:
@echo "=== Testing kogral-core (fastembed) ==="
cargo test --package kogral-core --features fastembed
# === INTEGRATION & DOCS ===
# Run integration tests only
[doc("Run integration tests")]
integration:
@echo "=== Running integration tests ==="
cargo test --workspace --test '*'
# Test documentation examples
[doc("Test doc examples and doctests")]
doc:
@echo "=== Testing documentation examples ==="
cargo test --workspace --doc

206
schemas/README.md Normal file
View File

@ -0,0 +1,206 @@
# Knowledge Base Nickel Schemas
This directory contains Nickel schema definitions for the knowledge base configuration system.
## Overview
The KOGRAL uses a **config-driven architecture** with Nickel providing type-safe configuration:
```text
.ncl files → nickel export --format json → JSON → serde → Rust structs
```
**Benefits:**
- Type safety at config definition time (Nickel type checker)
- Composition and reuse (Nickel imports)
- Runtime type-safe Rust structs (serde)
- Clear contracts and documentation
## Schema Files
| File | Purpose |
| --- | --- |
| `types.ncl` | Shared type definitions (enums, primitives) |
| `kogral-config.ncl` | Main configuration schema (KbConfig) |
| `frontmatter.ncl` | Document frontmatter schema (YAML in .md files) |
## Usage
### Export Schema to JSON
```bash
# Export a configuration file
nickel export --format json config/defaults.ncl > .kogral/config.json
# Export minimal config
nickel export --format json config/minimal.ncl > .kogral/config.json
# Export production config
nickel export --format json config/production.ncl > .kogral/config.json
```
### Create Custom Configuration
```nickel
# my-kogral-config.ncl
let Schema = import "schemas/kogral-config.ncl" in
{
graph = {
name = "my-project",
version = "1.0.0",
},
embeddings = {
provider = 'ollama,
model = "llama2",
},
# Other fields use defaults
} | Schema.KbConfig
```
### Type Checking
Nickel will validate your configuration against the schema:
```bash
# Check configuration without exporting
nickel typecheck my-kogral-config.ncl
# Export (also type-checks)
nickel export --format json my-kogral-config.ncl
```
## Schema Structure
### Main Config (`kogral-config.ncl`)
```nickel
KbConfig = {
graph: GraphConfig, # Graph metadata
inheritance: InheritanceConfig, # Guideline inheritance
storage: StorageConfig, # Storage backends
embeddings: EmbeddingConfig, # Embedding providers
templates: TemplateConfig, # Tera templates
query: QueryConfig, # Search behavior
mcp: McpConfig, # MCP server settings
sync: SyncConfig, # Filesystem ↔ DB sync
}
```
### Frontmatter (`frontmatter.ncl`)
```nickel
Frontmatter = {
id: String, # UUID
type: NodeType, # note, decision, etc.
title: String, # Human-readable title
created: Timestamp, # ISO 8601
modified: Timestamp, # ISO 8601
tags: Array String, # Categorization
status: NodeStatus, # draft, active, etc.
relates_to: Array String, # Relationships
# ... type-specific fields
}
```
## Examples
### Default Configuration
See `config/defaults.ncl` for a fully documented example with sensible defaults.
### Minimal Configuration
```nickel
{
graph = { name = "my-kogral" },
} | Schema.KbConfig
```
All other fields will use schema defaults.
### Production Configuration
See `config/production.ncl` for a production setup with:
- SurrealDB backend enabled
- API-based embeddings (OpenAI)
- Optimized sync settings
## Field Defaults
All config fields have sensible defaults. Required fields:
- `graph.name` - Graph identifier
Optional fields (with defaults):
- `graph.version``"1.0.0"`
- `storage.primary``'filesystem`
- `embeddings.provider``'fastembed`
- `query.similarity_threshold``0.4`
- `mcp.server.transport``'stdio`
- ... (see schema files for complete list)
## Type Definitions
### Enums
Defined in `types.ncl`:
```nickel
NodeType = [| 'note, 'decision, 'guideline, 'pattern, 'journal, 'execution |]
NodeStatus = [| 'draft, 'active, 'superseded, 'archived |]
StorageType = [| 'filesystem, 'memory |]
EmbeddingProvider = [| 'openai, 'claude, 'ollama, 'fastembed |]
```
### Primitives
- `String` - Text values
- `Number` - Numeric values
- `Bool` - Boolean values
- `Array T` - Lists of type T
- `{ _ | T }` - Map/dictionary with values of type T
## Validation
Nickel enforces:
- **Required fields**: Must be present
- **Type constraints**: Values must match declared types
- **Enum values**: Must be one of the allowed variants
- **Default values**: Applied automatically if field omitted
Example error:
```bash
$ nickel export invalid-config.ncl
error: type error
┌─ invalid-config.ncl:5:15
5 │ provider = 'gpt4,
│ ^^^^^ this expression has type [| 'gpt4 |]
= Expected an expression of type [| 'openai, 'claude, 'ollama, 'fastembed |]
```
## Integration with Rust
The Rust code loads JSON via serde:
```rust
use kb_core::config::loader::load_config;
// Load from .kogral/config.{ncl,toml,json}
let config = load_config(None, None)?;
// Access type-safe fields
println!("Graph: {}", config.graph.name);
println!("Provider: {:?}", config.embeddings.provider);
```
## References
- [Nickel Language](https://nickel-lang.org/)
- [Nickel Documentation](https://nickel-lang.org/user-manual/)
- Rust schema types: `crates/kogral-core/src/config/schema.rs`

180
schemas/frontmatter.ncl Normal file
View File

@ -0,0 +1,180 @@
# Document Frontmatter Schema
#
# Schema for YAML frontmatter in knowledge base documents.
# This frontmatter is embedded in markdown files and parsed by kogral-core.
#
# Format:
# ---
# <frontmatter fields>
# ---
#
# <markdown content>
let Types = import "types.ncl" in
{
# Frontmatter for knowledge base documents
Frontmatter = {
id
| String
| doc "Unique identifier (UUID)",
type
| Types.NodeType
| doc "Node type (note, decision, guideline, etc.)",
title
| String
| doc "Human-readable title",
created
| Types.Timestamp
| doc "Creation timestamp (ISO 8601)",
modified
| Types.Timestamp
| doc "Last modification timestamp (ISO 8601)",
tags
| Array String
| doc "Tags for categorization"
| default = [],
status
| Types.NodeStatus
| doc "Current status (draft, active, etc.)"
| default = 'draft,
# Relationship fields (compatible with Logseq [[wikilinks]])
relates_to
| Array String
| doc "Related node IDs"
| default = [],
depends_on
| Array String
| doc "Dependency node IDs (must exist/be read first)"
| default = [],
implements
| Array String
| doc "Implementation node IDs (this implements those patterns)"
| default = [],
extends
| Array String
| doc "Extension node IDs (this extends/overrides those)"
| default = [],
# Cross-project reference
project
| String
| doc "Project identifier (for cross-project links)"
| optional,
# Decision-specific fields (ADR format)
context
| String
| doc "Decision context (what problem are we solving?)"
| optional,
decision
| String
| doc "The decision made"
| optional,
consequences
| Array String
| doc "Consequences of the decision"
| optional,
# Guideline-specific fields
language
| String
| doc "Programming language (rust, nushell, etc.)"
| optional,
category
| String
| doc "Category (error-handling, testing, etc.)"
| optional,
# Pattern-specific fields
problem
| String
| doc "Problem statement"
| optional,
solution
| String
| doc "Solution description"
| optional,
forces
| Array String
| doc "Forces/constraints affecting the pattern"
| optional,
# Execution-specific fields (from Vapora KG)
task_type
| String
| doc "Type of task executed"
| optional,
agent
| String
| doc "Agent that executed the task"
| optional,
outcome
| String
| doc "Execution outcome (success, failure, etc.)"
| optional,
duration_ms
| Number
| doc "Execution duration in milliseconds"
| optional,
},
# Minimal frontmatter (required fields only)
MinimalFrontmatter = {
id | String,
type | Types.NodeType,
title | String,
created | Types.Timestamp,
modified | Types.Timestamp,
},
# Decision frontmatter (ADR)
DecisionFrontmatter = Frontmatter & {
type | default = 'decision,
context | String,
decision | String,
consequences | Array String | default = [],
},
# Guideline frontmatter
GuidelineFrontmatter = Frontmatter & {
type | default = 'guideline,
language | String,
category | String | optional,
},
# Pattern frontmatter
PatternFrontmatter = Frontmatter & {
type | default = 'pattern,
problem | String,
solution | String,
forces | Array String | default = [],
},
# Execution record frontmatter
ExecutionFrontmatter = Frontmatter & {
type | default = 'execution,
task_type | String,
agent | String | optional,
outcome | String,
duration_ms | Number | optional,
},
}

387
schemas/kogral-config.ncl Normal file
View File

@ -0,0 +1,387 @@
# Knowledge Base Configuration Schema
#
# Main configuration schema for the knowledge base system.
# This schema is exported to JSON and loaded into Rust structs.
#
# Usage:
# nickel export --format json kogral-config.ncl > config.json
let Types = import "types.ncl" in
{
# Graph metadata configuration
GraphConfig = {
name
| String
| doc "Graph name/identifier",
version
| Types.Version
| doc "Graph version (semver)"
| default = "1.0.0",
description
| String
| doc "Human-readable description"
| default = "",
},
# Inheritance configuration for guidelines
InheritanceConfig = {
base
| Types.Path
| doc "Base path for shared KOGRAL (resolves $TOOLS_PATH at runtime, defaults to $HOME/Tools)"
| default = "$TOOLS_PATH/.kogral-shared",
guidelines
| Array Types.Path
| doc "Additional guideline paths to inherit"
| default = [],
priority
| Types.PositiveInt
| doc "Override priority (higher = wins)"
| default = 100,
},
# Secondary storage configuration
SecondaryStorageConfig = {
enabled
| Bool
| doc "Whether secondary storage is enabled"
| default = false,
type
| Types.SecondaryStorageType
| doc "Secondary storage backend type"
| default = 'surrealdb,
url
| Types.Url
| doc "Connection URL for secondary storage"
| default = "ws://localhost:8000",
namespace
| String
| doc "Database namespace"
| default = "kogral",
database
| String
| doc "Database name"
| default = "default",
},
# Storage backend configuration
StorageConfig = {
primary
| Types.StorageType
| doc "Primary storage backend type"
| default = 'filesystem,
secondary
| SecondaryStorageConfig
| doc "Optional secondary storage (for scaling/search)"
| default = {},
},
# Embedding provider configuration
EmbeddingConfig = {
enabled
| Bool
| doc "Whether embeddings are enabled"
| default = true,
provider
| Types.EmbeddingProvider
| doc "Embedding provider selection"
| default = 'fastembed,
model
| String
| doc "Model name/identifier"
| default = "BAAI/bge-small-en-v1.5",
dimensions
| Types.PositiveInt
| doc "Vector dimensions"
| default = 384,
api_key_env
| String
| doc "Environment variable name for API key"
| default = "OPENAI_API_KEY",
},
# Logseq blocks support configuration
BlocksConfig = {
enabled
| Bool
| doc "Enable Logseq content blocks parsing and queries"
| default = false,
parse_on_import
| Bool
| doc "Automatically parse blocks when importing Logseq pages"
| default = true,
serialize_on_export
| Bool
| doc "Serialize blocks to outliner format on export"
| default = true,
enable_mcp_tools
| Bool
| doc "Enable block-related MCP tools (kogral/find_blocks, kogral/find_todos, kogral/find_cards)"
| default = true,
},
# Template mappings per node type
TemplateMap = {
note
| String
| doc "Note template filename"
| default = "note.md.tera",
decision
| String
| doc "Decision (ADR) template filename"
| default = "decision.md.tera",
guideline
| String
| doc "Guideline template filename"
| default = "guideline.md.tera",
pattern
| String
| doc "Pattern template filename"
| default = "pattern.md.tera",
journal
| String
| doc "Journal (daily notes) template filename"
| default = "journal.md.tera",
execution
| String
| doc "Execution record template filename"
| default = "execution.md.tera",
},
# Export template mappings
ExportTemplateMap = {
logseq_page
| String
| doc "Logseq page export template"
| default = "export/logseq-page.md.tera",
logseq_journal
| String
| doc "Logseq journal export template"
| default = "export/logseq-journal.md.tera",
summary
| String
| doc "Summary report template"
| default = "export/summary.md.tera",
json
| String
| doc "JSON export template"
| default = "export/graph.json.tera",
},
# Template engine configuration
TemplateConfig = {
templates_dir
| Types.Path
| doc "Template directory path (relative to project root)"
| default = "templates",
templates
| TemplateMap
| doc "Node type template mappings"
| default = {},
export
| ExportTemplateMap
| doc "Export template mappings"
| default = {},
custom
| { _ | String }
| doc "Custom template registry (name → path)"
| default = {},
},
# Query behavior configuration
QueryConfig = {
similarity_threshold
| Types.UnitFloat
| doc "Minimum similarity threshold for matches (0.0 to 1.0)"
| default = 0.4,
max_results
| Types.PositiveInt
| doc "Maximum number of search results"
| default = 10,
recency_weight
| Number
| doc "Recency weight (higher = prefer more recent results)"
| default = 3.0,
cross_graph
| Bool
| doc "Whether cross-graph queries are enabled"
| default = true,
},
# MCP server configuration
McpServerConfig = {
name
| String
| doc "MCP server name"
| default = "kogral-mcp",
version
| Types.Version
| doc "MCP server version"
| default = "1.0.0",
transport
| Types.McpTransport
| doc "Transport protocol (stdio or SSE)"
| default = 'stdio,
},
# MCP tools configuration
McpToolsConfig = {
search
| Bool
| doc "Enable kogral/search tool"
| default = true,
add_note
| Bool
| doc "Enable kogral/add_note tool"
| default = true,
add_decision
| Bool
| doc "Enable kogral/add_decision tool"
| default = true,
link
| Bool
| doc "Enable kogral/link tool"
| default = true,
get_guidelines
| Bool
| doc "Enable kogral/get_guidelines tool"
| default = true,
export
| Bool
| doc "Enable kogral/export tool"
| default = true,
},
# MCP resources configuration
McpResourcesConfig = {
expose_project
| Bool
| doc "Expose project resources (kogral://project/*)"
| default = true,
expose_shared
| Bool
| doc "Expose shared resources (kogral://shared/*)"
| default = true,
},
# MCP configuration
McpConfig = {
server
| McpServerConfig
| doc "MCP server settings"
| default = {},
tools
| McpToolsConfig
| doc "MCP tool enablement"
| default = {},
resources
| McpResourcesConfig
| doc "MCP resource exposure"
| default = {},
},
# Sync configuration
SyncConfig = {
auto_index
| Bool
| doc "Auto-sync filesystem to secondary storage"
| default = true,
watch_paths
| Array String
| doc "Paths to watch for changes"
| default = ["notes", "decisions", "guidelines", "patterns", "journal"],
debounce_ms
| Types.PositiveInt
| doc "Debounce time in milliseconds"
| default = 500,
},
# Main configuration schema
KbConfig = {
graph
| GraphConfig
| doc "Graph metadata configuration",
inheritance
| InheritanceConfig
| doc "Inheritance settings for guidelines"
| default = {},
storage
| StorageConfig
| doc "Storage backend configuration"
| default = {},
embeddings
| EmbeddingConfig
| doc "Embedding provider configuration"
| default = {},
blocks
| BlocksConfig
| doc "Logseq blocks support configuration"
| default = {},
templates
| TemplateConfig
| doc "Template engine configuration"
| default = {},
query
| QueryConfig
| doc "Query behavior configuration"
| default = {},
mcp
| McpConfig
| doc "MCP server configuration"
| default = {},
sync
| SyncConfig
| doc "Sync settings"
| default = {},
},
}

View File

@ -0,0 +1,268 @@
# Knowledge Base Configuration Contracts (Schema Definitions)
#
# Pattern: Pure schema definitions using Nickel contracts
# Follows: provisioning/schemas pattern
{
# === CORE TYPES ===
GraphConfig = {
name | String
| doc "Graph name identifier",
version | String
| doc "Semantic version string"
| default = "1.0.0",
description | String
| doc "Human-readable description"
| default = "",
},
InheritanceConfig = {
base | String
| doc "Path to shared KOGRAL directory"
| default = "/Users/Akasha/Tools/.kogral-shared",
guidelines | Array String
| doc "Additional guideline paths to inherit"
| default = [],
priority | Number
| doc "Override priority (higher wins)"
| default = 100,
},
# === STORAGE ===
StorageType = [| 'filesystem, 'memory, 'surrealdb |],
SecondaryStorageConfig = {
enabled | Bool
| doc "Enable secondary storage backend"
| default = false,
type | [| 'surrealdb, 'sqlite |]
| doc "Secondary storage type"
| default = 'surrealdb,
url | String
| doc "Connection URL"
| default = "ws://localhost:8000",
namespace | String
| doc "SurrealDB namespace"
| default = "kb",
database | String
| doc "SurrealDB database name"
| default = "default",
username | String
| doc "Database username"
| optional,
password | String
| doc "Database password"
| optional,
},
StorageConfig = {
primary | StorageType
| doc "Primary storage backend"
| default = 'filesystem,
secondary | SecondaryStorageConfig
| doc "Optional secondary storage"
| default = { enabled = false },
},
# === EMBEDDINGS ===
EmbeddingProviderType = [| 'openai, 'claude, 'ollama, 'fastembed |],
EmbeddingConfig = {
enabled | Bool
| doc "Enable embedding generation"
| default = true,
provider | EmbeddingProviderType
| doc "Embedding provider"
| default = 'fastembed,
model | String
| doc "Model name/identifier"
| default = "BAAI/bge-small-en-v1.5",
dimensions | Number
| doc "Embedding vector dimensions"
| default = 384,
api_key_env | String
| doc "Environment variable for API key"
| default = "OPENAI_API_KEY",
},
# === TEMPLATES ===
DocumentTemplates = {
note | String
| default = "note.md.tera",
decision | String
| default = "decision.md.tera",
guideline | String
| default = "guideline.md.tera",
pattern | String
| default = "pattern.md.tera",
journal | String
| default = "journal.md.tera",
execution | String
| default = "execution.md.tera",
},
ExportTemplates = {
logseq_page | String
| default = "export/logseq-page.md.tera",
logseq_journal | String
| default = "export/logseq-journal.md.tera",
summary | String
| default = "export/summary.md.tera",
json | String
| default = "export/graph.json.tera",
},
TemplateConfig = {
templates_dir | String
| doc "Directory containing templates"
| default = "templates",
templates | DocumentTemplates
| doc "Template files for each node type"
| default = {},
export | ExportTemplates
| doc "Export format templates"
| default = {},
custom | { _ : String }
| doc "Custom template registry (name → path)"
| default = {},
},
# === QUERY ===
QueryConfig = {
similarity_threshold | Number
| doc "Minimum similarity for semantic matches (0-1)"
| default = 0.4,
max_results | Number
| doc "Maximum results to return"
| default = 10,
recency_weight | Number
| doc "Weight factor for recent documents"
| default = 3.0,
cross_graph | Bool
| doc "Enable cross-graph queries"
| default = true,
},
# === MCP ===
McpServerConfig = {
name | String
| default = "kogral-mcp",
version | String
| default = "1.0.0",
transport | [| 'stdio, 'sse |]
| default = 'stdio,
},
McpToolsConfig = {
search | Bool | default = true,
add_note | Bool | default = true,
add_decision | Bool | default = true,
link | Bool | default = true,
get_guidelines | Bool | default = true,
export | Bool | default = true,
},
McpResourcesConfig = {
expose_project | Bool | default = true,
expose_shared | Bool | default = true,
},
McpConfig = {
server | McpServerConfig
| default = {},
tools | McpToolsConfig
| default = {},
resources | McpResourcesConfig
| default = {},
},
# === SYNC ===
SyncConfig = {
auto_index | Bool
| doc "Automatically sync filesystem to storage"
| default = true,
watch_paths | Array String
| doc "Directories to watch for changes"
| default = ["notes", "decisions", "guidelines", "patterns", "journal"],
debounce_ms | Number
| doc "Debounce delay for file system events"
| default = 500,
},
# === MAIN CONFIG ===
KbConfig = {
graph | GraphConfig
| doc "Graph metadata configuration",
inheritance | InheritanceConfig
| doc "Inheritance configuration"
| default = {},
storage | StorageConfig
| doc "Storage backend configuration"
| default = {},
embeddings | EmbeddingConfig
| doc "Embedding provider configuration"
| default = {},
templates | TemplateConfig
| doc "Template system configuration"
| default = {},
query | QueryConfig
| doc "Query engine configuration"
| default = {},
mcp | McpConfig
| doc "MCP server configuration"
| default = {},
sync | SyncConfig
| doc "Sync configuration"
| default = {},
},
}

View File

@ -0,0 +1,97 @@
# Knowledge Base Default Configuration Values
#
# Pattern: Default values for all configuration options
# These are base values that modes can override
let contracts = import "contracts.ncl" in
{
# Base configuration with all defaults
base = {
graph = {
name = "knowledge-base",
version = "1.0.0",
description = "Knowledge Base graph",
},
# Inheritance paths: set via TOOLS_PATH env var at export time
# Default paths resolve: $TOOLS_PATH/.kogral-shared (or $HOME/Tools/.kogral-shared)
inheritance = {
# Paths with $TOOLS_PATH are resolved at runtime by Rust code
base = "$TOOLS_PATH/.kogral-shared",
guidelines = [],
priority = 100,
},
storage = {
primary = 'filesystem,
secondary = {
enabled = false,
type = 'surrealdb,
url = "ws://localhost:8000",
namespace = "kogral",
database = "default",
},
},
embeddings = {
enabled = true,
provider = 'fastembed,
model = "BAAI/bge-small-en-v1.5",
dimensions = 384,
api_key_env = "OPENAI_API_KEY",
},
templates = {
templates_dir = "templates",
templates = {
note = "note.md.tera",
decision = "decision.md.tera",
guideline = "guideline.md.tera",
pattern = "pattern.md.tera",
journal = "journal.md.tera",
execution = "execution.md.tera",
},
export = {
logseq_page = "export/logseq-page.md.tera",
logseq_journal = "export/logseq-journal.md.tera",
summary = "export/summary.md.tera",
json = "export/graph.json.tera",
},
custom = {},
},
query = {
similarity_threshold = 0.4,
max_results = 10,
recency_weight = 3.0,
cross_graph = true,
},
mcp = {
server = {
name = "kogral-mcp",
version = "1.0.0",
transport = 'stdio,
},
tools = {
search = true,
add_note = true,
add_decision = true,
link = true,
get_guidelines = true,
export = true,
},
resources = {
expose_project = true,
expose_shared = true,
},
},
sync = {
auto_index = true,
watch_paths = ["notes", "decisions", "guidelines", "patterns", "journal"],
debounce_ms = 500,
},
} | contracts.KbConfig,
}

111
schemas/kogral/helpers.ncl Normal file
View File

@ -0,0 +1,111 @@
# Knowledge Base Configuration Composition Helpers
#
# Provides utilities for merging configurations from multiple layers:
# 1. Schema (type contracts)
# 2. Defaults (base values)
# 3. Mode Overlay (mode-specific tuning: dev/prod/test)
# 4. User Customization (overrides)
#
# Pattern: Follows provisioning/schemas/platform/common/helpers.ncl
{
# Recursively merge two record configurations
# Override values take precedence over base (shallow merge at each level)
#
# Example:
# let base = { storage = { primary = 'filesystem }, embeddings = { enabled = true } }
# let override = { storage = { primary = 'surrealdb } }
# merge_with_override base override
# # Result: { storage = { primary = 'surrealdb }, embeddings = { enabled = true } }
merge_with_override | not_exported = fun base override =>
if std.is_record base && std.is_record override then
let base_fields = std.record.fields base in
let override_fields = std.record.fields override in
base_fields
|> std.array.fold_right
(fun key acc =>
let base_value = base."%{key}" in
if std.record.has_field key override then
let override_value = override."%{key}" in
if std.is_record base_value && std.is_record override_value then
acc
& { "%{key}" = merge_with_override base_value override_value }
else
# Override value takes precedence
acc & { "%{key}" = override_value }
else
# Keep base value
acc & { "%{key}" = base_value }
)
(override_fields
|> std.array.fold_right
(fun key acc =>
if !std.record.has_field key base then
acc & { "%{key}" = override."%{key}" }
else
acc
)
{}
)
else
# If either is not a record, override takes precedence
if std.is_null override then base else override,
# Compose configuration from multiple layers with proper merging
#
# Layer 1: defaults (base values)
# Layer 2: mode_config (mode-specific overrides: dev/prod/test)
# Layer 3: user_custom (user customizations)
#
# Example:
# let defaults = { embeddings = { provider = 'fastembed } }
# let mode = { embeddings = { provider = 'openai } } # Production override
# let user = { graph = { name = "my-project" } }
# compose_config defaults mode user
compose_config | not_exported = fun defaults mode_config user_custom =>
let with_mode = merge_with_override defaults mode_config in
merge_with_override with_mode user_custom,
# Compose minimal config (defaults + user only, no mode)
# Useful for simple cases where mode-specific tuning isn't needed
compose_minimal | not_exported = fun defaults user_custom =>
merge_with_override defaults user_custom,
# Validate that required fields are present
# Returns config if valid, throws error if invalid
validate_required | not_exported = fun config required_fields =>
required_fields
|> std.array.fold_right
(fun field acc =>
if std.record.has_field field config then
acc
else
std.fail "Required field missing: %{field}"
)
config,
# Extract specific subsection from config
# Example: extract_section config "storage"
extract_section | not_exported = fun config section =>
if std.record.has_field section config then
config."%{section}"
else
std.fail "Section not found: %{section}",
# Merge arrays (for things like watch_paths, tags, etc.)
# Deduplicates and preserves order
merge_arrays | not_exported = fun base override =>
let combined = base @ override in
std.array.fold_right
(fun item acc =>
if std.array.elem item acc then
acc
else
[item] @ acc
)
[]
combined,
}

View File

@ -0,0 +1,48 @@
# Development Mode Configuration Overlay
#
# Optimized for: Fast iteration, local development, debugging
# Storage: Filesystem only (git-tracked)
# Embeddings: Local fastembed (no API costs)
# Logging: Debug level
# Sync: Disabled (manual only)
{
storage = {
primary = 'filesystem,
secondary = {
enabled = false, # No database in dev mode
},
},
embeddings = {
enabled = true,
provider = 'fastembed, # Local, no API costs
model = "BAAI/bge-small-en-v1.5",
dimensions = 384,
},
query = {
similarity_threshold = 0.4, # Permissive for exploration
max_results = 20, # More results for discovery
cross_graph = true,
},
sync = {
auto_index = false, # Manual sync in dev
debounce_ms = 1000, # Longer debounce
},
mcp = {
server = {
transport = 'stdio,
},
tools = {
search = true,
add_note = true,
add_decision = true,
link = true,
get_guidelines = true,
export = true,
},
},
}

View File

@ -0,0 +1,57 @@
# Production Mode Configuration Overlay
#
# Optimized for: Scalability, performance, reliability
# Storage: Hybrid (filesystem + SurrealDB)
# Embeddings: Cloud providers (OpenAI/Claude via rig-core)
# Logging: Info level
# Sync: Auto-enabled with optimized debounce
{
storage = {
primary = 'filesystem,
secondary = {
enabled = true, # SurrealDB for scalable queries
type = 'surrealdb,
url = "ws://localhost:8000",
namespace = "kogral",
database = "production",
},
},
embeddings = {
enabled = true,
provider = 'openai, # Cloud API for production quality
model = "text-embedding-3-small",
dimensions = 1536,
api_key_env = "OPENAI_API_KEY",
},
query = {
similarity_threshold = 0.6, # Stricter for quality results
max_results = 10, # Conservative for performance
cross_graph = true,
},
sync = {
auto_index = true, # Auto-sync enabled
debounce_ms = 300, # Faster response to changes
},
mcp = {
server = {
transport = 'stdio,
},
tools = {
search = true,
add_note = true,
add_decision = true,
link = true,
get_guidelines = true,
export = true,
},
resources = {
expose_project = true,
expose_shared = true,
},
},
}

View File

@ -0,0 +1,52 @@
# Test Mode Configuration Overlay
#
# Optimized for: Fast tests, isolation, no side effects
# Storage: In-memory only (ephemeral)
# Embeddings: Disabled (tests don't need semantic search)
# Logging: Debug level
# Sync: Disabled (manual control in tests)
{
storage = {
primary = 'memory, # Ephemeral, fast, isolated
secondary = {
enabled = false, # No database in tests
},
},
embeddings = {
enabled = false, # Disable for test speed
provider = 'fastembed, # Fallback if needed
model = "BAAI/bge-small-en-v1.5",
dimensions = 384,
},
query = {
similarity_threshold = 0.3, # Permissive for test coverage
max_results = 50, # More results for verification
cross_graph = false, # Isolated tests
},
sync = {
auto_index = false, # Manual control in tests
debounce_ms = 0, # No debounce for deterministic tests
},
mcp = {
server = {
transport = 'stdio,
},
tools = {
search = true,
add_note = true,
add_decision = true,
link = true,
get_guidelines = true,
export = true,
},
resources = {
expose_project = false, # Isolated tests
expose_shared = false,
},
},
}

View File

@ -0,0 +1,139 @@
-- SurrealDB Schema for Logseq Blocks Support
-- This schema extends the KOGRAL system to support block-level storage and querying
-- Define namespace and database
-- USE NS kogral;
-- USE DB default;
-- ============================================================================
-- BLOCK TABLE
-- ============================================================================
DEFINE TABLE block SCHEMAFULL;
-- Fields
DEFINE FIELD node_id ON block TYPE record(node) ASSERT $value != NONE;
DEFINE FIELD block_id ON block TYPE string ASSERT $value != NONE;
DEFINE FIELD content ON block TYPE string;
DEFINE FIELD parent_id ON block TYPE option<string>;
DEFINE FIELD created ON block TYPE datetime;
DEFINE FIELD modified ON block TYPE datetime;
-- Properties (flexible object for block metadata)
DEFINE FIELD properties ON block TYPE object;
DEFINE FIELD properties.tags ON block TYPE array;
DEFINE FIELD properties.status ON block TYPE option<string>;
DEFINE FIELD properties.custom ON block TYPE option<object>;
DEFINE FIELD properties.block_refs ON block TYPE array;
DEFINE FIELD properties.page_refs ON block TYPE array;
-- ============================================================================
-- INDEXES
-- ============================================================================
-- Index on node_id for fast lookup of all blocks in a node
DEFINE INDEX block_node_idx ON block COLUMNS node_id;
-- Index on block_id for direct block lookup
DEFINE INDEX block_id_idx ON block COLUMNS block_id UNIQUE;
-- Index on tags for tag-based queries (find all #card blocks)
DEFINE INDEX block_tags_idx ON block COLUMNS properties.tags;
-- Index on status for TODO/DONE queries
DEFINE INDEX block_status_idx ON block COLUMNS properties.status;
-- Index on parent_id for hierarchical queries
DEFINE INDEX block_parent_idx ON block COLUMNS parent_id;
-- Full-text search index on content
DEFINE INDEX block_content_search ON block COLUMNS content SEARCH ANALYZER simple BM25;
-- ============================================================================
-- QUERIES (Examples)
-- ============================================================================
-- Find all blocks with a specific tag
-- SELECT * FROM block WHERE $tag IN properties.tags;
-- Find all TODO blocks
-- SELECT * FROM block WHERE properties.status = "TODO";
-- Find all blocks in a specific node
-- SELECT * FROM block WHERE node_id = $node_record_id;
-- Find blocks with custom property
-- SELECT * FROM block WHERE properties.custom[$key] = $value;
-- Full-text search in block content
-- SELECT * FROM block WHERE content @@ $search_term;
-- Get block hierarchy (parent and all children)
-- SELECT *,
-- (SELECT * FROM block WHERE parent_id = $parent.block_id) AS children
-- FROM block WHERE block_id = $parent_block_id;
-- ============================================================================
-- RELATIONSHIPS
-- ============================================================================
-- Block belongs to a Node
DEFINE FIELD node_id ON block TYPE record(node);
-- Example: Get all blocks for a node with their content
-- SELECT * FROM block WHERE node_id = $node_id ORDER BY created ASC;
-- Example: Count blocks by tag across all nodes
-- SELECT
-- array::flatten(properties.tags) AS tag,
-- count() AS count
-- FROM block
-- GROUP BY tag
-- ORDER BY count DESC;
-- Example: Get all flashcards (#card) across the knowledge base
-- SELECT
-- node_id.title AS node_title,
-- content,
-- properties
-- FROM block
-- WHERE "card" IN properties.tags;
-- ============================================================================
-- MIGRATION QUERIES
-- ============================================================================
-- Populate blocks table from existing nodes (run once)
-- This would be executed by the sync mechanism or migration script
-- Example migration pseudocode:
-- FOR node IN (SELECT * FROM node) {
-- LET parsed_blocks = parse_markdown(node.content);
-- FOR block IN parsed_blocks {
-- CREATE block CONTENT {
-- node_id: node.id,
-- block_id: block.id,
-- content: block.content,
-- parent_id: block.parent_id,
-- created: block.created,
-- modified: block.modified,
-- properties: {
-- tags: block.properties.tags,
-- status: block.properties.status,
-- custom: block.properties.custom,
-- block_refs: block.properties.block_refs,
-- page_refs: block.properties.page_refs
-- }
-- };
-- }
-- }
-- ============================================================================
-- CLEANUP QUERIES
-- ============================================================================
-- Delete all blocks for a specific node
-- DELETE block WHERE node_id = $node_record_id;
-- Delete orphaned blocks (node no longer exists)
-- DELETE block WHERE node_id NOT IN (SELECT id FROM node);

47
schemas/types.ncl Normal file
View File

@ -0,0 +1,47 @@
# Shared types for Knowledge Base configuration
#
# This file defines common types used across KB configuration schemas.
{
# Node types for knowledge base entries
NodeType = [| 'note, 'decision, 'guideline, 'pattern, 'journal, 'execution |],
# Node status values
NodeStatus = [| 'draft, 'active, 'superseded, 'archived |],
# Relationship edge types
EdgeType = [| 'relates_to, 'depends_on, 'implements, 'extends, 'supersedes, 'explains |],
# Storage backend types
StorageType = [| 'filesystem, 'memory |],
# Secondary storage backend types
SecondaryStorageType = [| 'surrealdb, 'sqlite |],
# Embedding provider types
EmbeddingProvider = [| 'openai, 'claude, 'ollama, 'fastembed |],
# MCP transport types
McpTransport = [| 'stdio, 'sse |],
# ISO 8601 timestamp string
Timestamp = String,
# Semantic version string (e.g., "1.0.0")
Version = String,
# File path string
Path = String,
# URL string
Url = String,
# Positive integer
PositiveInt = Number,
# Float between 0.0 and 1.0
UnitFloat = Number,
# Email address
Email = String,
}

164
scripts/kogral-backup.nu Normal file
View File

@ -0,0 +1,164 @@
#!/usr/bin/env nu
# Backup KOGRAL graphs to archive
#
# Usage: nu kogral-backup.nu [--output <path>] [--format <tar|zip>] [--compress]
def main [
--output: string = "kogral-backup" # Output filename (without extension)
--format: string = "tar" # Archive format: tar or zip
--compress # Compress the archive
--kogral-dir: string = ".kogral" # KOGRAL directory
--include-metadata # Include .git and other metadata
] {
print $"(ansi green_bold)KOGRAL Backup(ansi reset)"
print $"KOGRAL Directory: ($kogral-dir)"
print $"Format: ($format)"
print $"Compress: ($compress)"
# Check if .kogral directory exists
if not ($kogral-dir | path exists) {
print $"(ansi red)Error: KOGRAL directory not found: ($kogral-dir)(ansi reset)"
exit 1
}
# Generate timestamp
let timestamp = date now | format date "%Y%m%d_%H%M%S"
let backup_name = $"($output)_($timestamp)"
# Determine file extension
let extension = if $compress {
if $format == "tar" { ".tar.gz" } else { ".zip" }
} else {
if $format == "tar" { ".tar" } else { ".zip" }
}
let backup_file = $"($backup_name)($extension)"
print $"\n(ansi cyan_bold)Preparing backup...(ansi reset)"
print $"Output file: ($backup_file)"
# Count files
let stats = get_kogral_stats $kogral_dir
print $"\n(ansi cyan_bold)Files to backup:(ansi reset)"
print $" Notes: ($stats.notes)"
print $" Decisions: ($stats.decisions)"
print $" Guidelines: ($stats.guidelines)"
print $" Patterns: ($stats.patterns)"
print $" Journal: ($stats.journal)"
print $" Config: ($stats.config)"
print $" Total: ($stats.total)"
# Create backup
print $"\n(ansi cyan_bold)Creating backup...(ansi reset)"
match $format {
"tar" => {
create_tar_backup $kogral_dir $backup_file $compress $include_metadata
},
"zip" => {
create_zip_backup $kogral_dir $backup_file $include_metadata
},
_ => {
print $"(ansi red)Error: Invalid format. Use: tar or zip(ansi reset)"
exit 1
}
}
# Verify backup
if ($backup_file | path exists) {
let size = ls $backup_file | get size | first
print $"\n(ansi green_bold)✓ Backup created successfully(ansi reset)"
print $"File: ($backup_file)"
print $"Size: ($size)"
} else {
print $"\n(ansi red)✗ Backup creation failed(ansi reset)"
exit 1
}
# Generate manifest
print $"\n(ansi cyan_bold)Generating manifest...(ansi reset)"
let manifest = generate_manifest $kogral_dir $backup_file $stats $timestamp
let manifest_file = $"($backup_name).manifest.json"
$manifest | to json | save $manifest_file
print $"Manifest saved: ($manifest_file)"
print $"\n(ansi green_bold)✓ Backup completed(ansi reset)"
}
def get_kogral_stats [kogral_dir: string] -> record {
let notes = (glob $"($kogral_dir)/notes/**/*.md" | length)
let decisions = (glob $"($kogral_dir)/decisions/**/*.md" | length)
let guidelines = (glob $"($kogral_dir)/guidelines/**/*.md" | length)
let patterns = (glob $"($kogral_dir)/patterns/**/*.md" | length)
let journal = (glob $"($kogral_dir)/journal/**/*.md" | length)
let config = if ($"($kogral_dir)/config.toml" | path exists) { 1 } else { 0 }
{
notes: $notes,
decisions: $decisions,
guidelines: $guidelines,
patterns: $patterns,
journal: $journal,
config: $config,
total: ($notes + $decisions + $guidelines + $patterns + $journal + $config)
}
}
def create_tar_backup [kogral_dir: string, output: string, compress: bool, include_metadata: bool] {
let compress_flag = if $compress { "z" } else { "" }
let exclude_flags = if not $include_metadata {
["--exclude=.git", "--exclude=.DS_Store"]
} else {
[]
}
print $" Creating tar archive..."
# Use tar command
let tar_cmd = if $compress {
$"tar -c($compress_flag)f ($output) ($exclude_flags | str join ' ') ($kogral_dir)"
} else {
$"tar -cf ($output) ($exclude_flags | str join ' ') ($kogral_dir)"
}
try {
bash -c $tar_cmd
print $" (ansi green)✓ Tar archive created(ansi reset)"
} catch {
print $" (ansi red)✗ Tar archive creation failed(ansi reset)"
}
}
def create_zip_backup [kogral_dir: string, output: string, include_metadata: bool] {
print $" Creating zip archive..."
let exclude_pattern = if not $include_metadata { "*.git* */.DS_Store" } else { "" }
try {
bash -c $"zip -r ($output) ($kogral_dir) -x ($exclude_pattern)"
print $" (ansi green)✓ Zip archive created(ansi reset)"
} catch {
print $" (ansi red)✗ Zip archive creation failed(ansi reset)"
}
}
def generate_manifest [kogral_dir: string, backup_file: string, stats: record, timestamp: string] -> record {
let config_path = $"($kogral_dir)/config.toml"
let config = if ($config_path | path exists) {
open $config_path | from toml
} else {
{ graph: { name: "unknown", version: "unknown" } }
}
{
backup_timestamp: $timestamp,
backup_file: $backup_file,
kogral_directory: $kogral_dir,
graph_name: $config.graph.name,
graph_version: $config.graph.version,
statistics: $stats,
created_by: "kogral-backup.nu",
version: "1.0.0"
}
}

View File

@ -0,0 +1,337 @@
#!/usr/bin/env nu
# Export KOGRAL to Logseq format
#
# Usage: nu kogral-export-logseq.nu <output-path> [--kogral-dir <path>] [--dry-run]
def main [
output_path: string # Path for Logseq graph output
--kogral-dir: string = ".kogral" # KOGRAL directory
--dry-run # Show what would be exported without making changes
--skip-journals # Skip exporting journal entries
] {
print $"(ansi green_bold)Logseq Export(ansi reset)"
print $"Source: ($kogral_dir)"
print $"Target: ($output_path)"
if $dry_run {
print $"(ansi yellow)DRY RUN MODE - No changes will be made(ansi reset)"
}
# Check if .kogral directory exists
if not ($kogral_dir | path exists) {
print $"(ansi red)Error: KOGRAL directory not found: ($kogral_dir)(ansi reset)"
exit 1
}
# Count files to export
let stats = get_export_stats $kogral_dir $skip_journals
print $"\n(ansi cyan_bold)Files to export:(ansi reset)"
print $" Notes: ($stats.notes)"
print $" Decisions: ($stats.decisions)"
print $" Guidelines: ($stats.guidelines)"
print $" Patterns: ($stats.patterns)"
print $" Journals: ($stats.journals)"
print $" Total: ($stats.total)"
if $stats.total == 0 {
print $"\n(ansi yellow)No files to export(ansi reset)"
exit 0
}
if $dry_run {
print $"\n(ansi yellow)[DRY RUN] Would export ($stats.total) files(ansi reset)"
exit 0
}
# Create Logseq directory structure
print $"\n(ansi cyan_bold)Creating Logseq directory structure...(ansi reset)"
create_logseq_structure $output_path
# Export files
print $"\n(ansi cyan_bold)Exporting files...(ansi reset)"
export_nodes $"($kogral_dir)/notes" $"($output_path)/pages" "note"
export_nodes $"($kogral_dir)/decisions" $"($output_path)/pages" "decision"
export_nodes $"($kogral_dir)/guidelines" $"($output_path)/pages" "guideline"
export_nodes $"($kogral_dir)/patterns" $"($output_path)/pages" "pattern"
if not $skip_journals {
export_journals $"($kogral_dir)/journal" $"($output_path)/journals"
}
# Create Logseq config
print $"\n(ansi cyan_bold)Creating Logseq configuration...(ansi reset)"
create_logseq_config $output_path
print $"\n(ansi green_bold)✓ Export completed(ansi reset)"
print $"Exported ($stats.total) files to ($output_path)"
}
def get_export_stats [kogral_dir: string, skip_journals: bool] {
let notes = if ($"($kogral_dir)/notes" | path exists) {
glob $"($kogral_dir)/notes/**/*.md" | length
} else { 0 }
let decisions = if ($"($kogral_dir)/decisions" | path exists) {
glob $"($kogral_dir)/decisions/**/*.md" | length
} else { 0 }
let guidelines = if ($"($kogral_dir)/guidelines" | path exists) {
glob $"($kogral_dir)/guidelines/**/*.md" | length
} else { 0 }
let patterns = if ($"($kogral_dir)/patterns" | path exists) {
glob $"($kogral_dir)/patterns/**/*.md" | length
} else { 0 }
let journals = if not $skip_journals and ($"($kogral_dir)/journal" | path exists) {
glob $"($kogral_dir)/journal/**/*.md" | length
} else { 0 }
{
notes: $notes,
decisions: $decisions,
guidelines: $guidelines,
patterns: $patterns,
journals: $journals,
total: ($notes + $decisions + $guidelines + $patterns + $journals)
}
}
def create_logseq_structure [output_path: string] {
mkdir $output_path
mkdir $"($output_path)/pages"
mkdir $"($output_path)/journals"
mkdir $"($output_path)/assets"
mkdir $"($output_path)/logseq"
print $" (ansi green)✓ Directory structure created(ansi reset)"
}
def export_nodes [source_dir: string, target_dir: string, node_type: string] {
if not ($source_dir | path exists) {
return
}
let files = glob $"($source_dir)/**/*.md"
if ($files | length) == 0 {
return
}
print $"\n Exporting ($node_type)s..."
mut exported = 0
let total = $files | length
for file in $files {
let filename = $file | path basename
# Phase 1: Read KOGRAL markdown file
let content = open $file
# Phase 2: Convert to Logseq format
let logseq_content = convert_kogral_to_logseq $content $node_type
# Phase 3: Save to Logseq pages directory
$logseq_content | save $"($target_dir)/($filename)"
$exported = $exported + 1
}
print $" (ansi green)✓ Exported ($exported)/($total) ($node_type)s(ansi reset)"
}
def export_journals [source_dir: string, target_dir: string] {
if not ($source_dir | path exists) {
return
}
let files = glob $"($source_dir)/**/*.md"
if ($files | length) == 0 {
return
}
print $"\n Exporting journals..."
mut exported = 0
let total = $files | length
for file in $files {
let filename = $file | path basename
# Phase 1: Read KOGRAL journal file
let content = open $file
# Phase 2: Convert to Logseq format
let logseq_content = convert_kogral_to_logseq $content "journal"
# Phase 3: Save to Logseq journals directory
$logseq_content | save $"($target_dir)/($filename)"
$exported = $exported + 1
}
print $" (ansi green)✓ Exported ($exported)/($total) journals(ansi reset)"
}
def convert_kogral_to_logseq [content: string, node_type: string] {
let lines = $content | lines
# Phase 1: Check for and parse YAML frontmatter
let has_frontmatter = ($lines | get 0 | str trim) == "---"
if not $has_frontmatter {
# No frontmatter, return content as-is with minimal properties
return $"type:: ($node_type)\n\n$content"
}
# Phase 2: Extract frontmatter and body
let frontmatter_end = get_frontmatter_end_index $lines
let frontmatter_lines = $lines | take $frontmatter_end
let body_lines = $lines | skip $frontmatter_end
# Phase 3: Parse YAML fields
let fm = parse_yaml_frontmatter $frontmatter_lines
# Phase 4: Convert to Logseq properties format
mut logseq_props = ""
# Add type property
$logseq_props = $logseq_props + $"type:: ($node_type)\n"
# Add title if present
if not ($fm.title? | is-empty) {
$logseq_props = $logseq_props + $"title:: ($fm.title?)\n"
}
# Add created date if present
if not ($fm.created? | is-empty) {
let created_date = convert_date_to_logseq $fm.created?
$logseq_props = $logseq_props + $"created:: [[$created_date]]\n"
}
# Add tags if present
if not ($fm.tags? | is-empty) {
$logseq_props = $logseq_props + "tags:: "
let tags_list = $fm.tags? | str replace '\[' '' | str replace '\]' '' | split row ','
for tag in $tags_list {
let trimmed = $tag | str trim | str replace '"' ''
$logseq_props = $logseq_props + $"[[($trimmed)]] "
}
$logseq_props = $logseq_props + "\n"
}
# Add status if present
if not ($fm.status? | is-empty) {
$logseq_props = $logseq_props + $"status:: ($fm.status?)\n"
}
# Add relationships if present
if not ($fm.relates_to? | is-empty) {
$logseq_props = $logseq_props + "relates-to:: "
let refs = parse_yaml_list $fm.relates_to?
let refs_formatted = $refs | each { |r| $'[[($r)]]' }
$logseq_props = $logseq_props + ($refs_formatted | str join ", ") + "\n"
}
if not ($fm.depends_on? | is-empty) {
$logseq_props = $logseq_props + "depends-on:: "
let refs = parse_yaml_list $fm.depends_on?
let refs_formatted = $refs | each { |r| $'[[($r)]]' }
$logseq_props = $logseq_props + ($refs_formatted | str join ", ") + "\n"
}
# Phase 5: Build final output
let body = $body_lines | str join "\n"
$"$logseq_props\n$body"
}
def get_frontmatter_end_index [lines: list] {
mut idx = 1 # Skip first "---"
for line in ($lines | skip 1) {
if ($line | str trim) == "---" {
return ($idx + 1)
}
$idx = $idx + 1
}
$idx
}
def parse_yaml_frontmatter [lines: list] {
mut fm = {}
for line in $lines {
if ($line | str trim) == "---" {
continue
}
# Match YAML key: value format
if ($line =~ '^[\w]+:') {
let key = $line | str replace '^(\w+):.*' '$1'
let value = $line | str replace '^[\w]+:\s*' '' | str trim
$fm = ($fm | insert $key $value)
}
}
$fm
}
def convert_date_to_logseq [date_str: string] {
# Convert ISO 8601 (2026-01-17T10:30:00Z) to Logseq format (Jan 17th, 2026)
# For simplicity, extract date part and format
let date_part = $date_str | str substring 0..10
let year = $date_part | str substring 0..4
let month = $date_part | str substring 5..7
let day = $date_part | str substring 8..10 | str replace '^0+' ''
let month_name = match $month {
"01" => "Jan",
"02" => "Feb",
"03" => "Mar",
"04" => "Apr",
"05" => "May",
"06" => "Jun",
"07" => "Jul",
"08" => "Aug",
"09" => "Sep",
"10" => "Oct",
"11" => "Nov",
"12" => "Dec",
_ => "Unknown"
}
let day_suffix = match ($day | into int) {
1 | 21 | 31 => "st",
2 | 22 => "nd",
3 | 23 => "rd",
_ => "th"
}
$"($month_name) ($day)($day_suffix), ($year)"
}
def parse_yaml_list [yaml_str: string] {
# Parse YAML list format: [item1, item2] or list format with dashes
# For now, handle bracket format
let cleaned = $yaml_str | str replace '\[' '' | str replace '\]' ''
let items = $cleaned | split row ',' | map { |i| $i | str trim | str replace '"' '' }
$items
}
def create_logseq_config [output_path: string] {
let config = {
"preferred-format": "markdown",
"preferred-workflow": ":now",
"hidden": [".git"],
"journal/page-title-format": "yyyy-MM-dd",
"start-of-week": 1,
"feature/enable-block-timestamps": false,
"feature/enable-search-remove-accents": true
}
$config | to json | save $"($output_path)/logseq/config.edn"
print $" (ansi green)✓ Logseq configuration created(ansi reset)"
}

View File

@ -0,0 +1,388 @@
#!/usr/bin/env nu
# Import from Logseq graph to KOGRAL
#
# Usage: nu kogral-import-logseq.nu <logseq-path> [--kogral-dir <path>] [--dry-run]
def main [
logseq_path: string # Path to Logseq graph directory
--kogral-dir: string = ".kogral" # KOGRAL directory
--dry-run # Show what would be imported without making changes
--skip-journals # Skip importing journal entries
--skip-pages # Skip importing pages
] {
print $"(ansi green_bold)Logseq Import(ansi reset)"
print $"Source: ($logseq_path)"
print $"Target: ($kogral_dir)"
if $dry_run {
print $"(ansi yellow)DRY RUN MODE - No changes will be made(ansi reset)"
}
# Validate Logseq directory
if not ($logseq_path | path exists) {
print $"(ansi red)Error: Logseq path not found: ($logseq_path)(ansi reset)"
exit 1
}
let pages_dir = $"($logseq_path)/pages"
let journals_dir = $"($logseq_path)/journals"
if not ($pages_dir | path exists) and not ($journals_dir | path exists) {
print $"(ansi red)Error: Not a valid Logseq graph (missing pages or journals directory)(ansi reset)"
exit 1
}
# Validate KOGRAL directory
if not ($kogral_dir | path exists) {
print $"(ansi yellow)KOGRAL directory doesn't exist. Creating...(ansi reset)"
if not $dry_run {
mkdir $kogral_dir
mkdir $"($kogral_dir)/notes"
mkdir $"($kogral_dir)/journal"
}
}
# Count files to import
let pages_count = if ($pages_dir | path exists) and not $skip_pages {
glob $"($pages_dir)/**/*.md" | length
} else { 0 }
let journals_count = if ($journals_dir | path exists) and not $skip_journals {
glob $"($journals_dir)/**/*.md" | length
} else { 0 }
print $"\n(ansi cyan_bold)Files to import:(ansi reset)"
print $" Pages: ($pages_count)"
print $" Journals: ($journals_count)"
print $" Total: ($pages_count + $journals_count)"
if ($pages_count + $journals_count) == 0 {
print $"\n(ansi yellow)No files to import(ansi reset)"
exit 0
}
if $dry_run {
print $"\n(ansi yellow)[DRY RUN] Would import ($pages_count + $journals_count) files(ansi reset)"
exit 0
}
# Import pages
if not $skip_pages and $pages_count > 0 {
print $"\n(ansi cyan_bold)Importing pages...(ansi reset)"
import_pages $pages_dir $kogral_dir
}
# Import journals
if not $skip_journals and $journals_count > 0 {
print $"\n(ansi cyan_bold)Importing journals...(ansi reset)"
import_journals $journals_dir $kogral_dir
}
print $"\n(ansi green_bold)✓ Import completed(ansi reset)"
print $"Imported ($pages_count + $journals_count) files"
}
def import_pages [pages_dir: string, kogral_dir: string] {
let files = glob $"($pages_dir)/**/*.md"
let total = $files | length
mut imported = 0
mut decisions = 0
mut guidelines = 0
mut patterns = 0
mut notes = 0
for file in $files {
let filename = $file | path basename
print $" Importing ($filename)..."
# Phase 1: Read and parse Logseq format
let content = open $file
# Phase 2: Detect node type from properties/content
let node_type = detect_node_type $content
# Phase 3: Convert to KOGRAL format
let kogral_content = convert_logseq_to_kogral $content $node_type
# Phase 4: Determine target directory
let target_dir = match $node_type {
"decision" => {
$decisions = $decisions + 1
$"($kogral_dir)/decisions"
},
"guideline" => {
$guidelines = $guidelines + 1
$"($kogral_dir)/guidelines"
},
"pattern" => {
$patterns = $patterns + 1
$"($kogral_dir)/patterns"
},
_ => {
$notes = $notes + 1
$"($kogral_dir)/notes"
}
}
# Phase 5: Save to KOGRAL
mkdir $target_dir
$kogral_content | save $"($target_dir)/($filename)"
$imported = $imported + 1
print $" (ansi green)✓ Imported as ($node_type)(ansi reset)"
}
print $"\n(ansi green)Pages summary:(ansi reset)"
print $" Notes: ($notes)"
print $" Decisions: ($decisions)"
print $" Guidelines: ($guidelines)"
print $" Patterns: ($patterns)"
print $" Total: ($imported)/($total) imported"
}
def import_journals [journals_dir: string, kogral_dir: string] {
let files = glob $"($journals_dir)/**/*.md"
let total = $files | length
mkdir $"($kogral_dir)/journal"
mut imported = 0
for file in $files {
let filename = $file | path basename
print $" Importing ($filename)..."
# Phase 1: Read Logseq journal format
let content = open $file
# Phase 2: Convert to KOGRAL journal format
let kogral_content = convert_logseq_to_kogral $content "journal"
# Phase 3: Save to journal directory
$kogral_content | save $"($kogral_dir)/journal/($filename)"
$imported = $imported + 1
print $" (ansi green)✓ Imported(ansi reset)"
}
print $"\n(ansi green)Journals imported: ($imported)/($total)(ansi reset)"
}
def detect_node_type [content: string] {
# Check for type hints in properties or content
if ($content | str contains "type:: decision") or ($content | str contains "# Decision") {
"decision"
} else if ($content | str contains "type:: guideline") or ($content | str contains "# Guideline") {
"guideline"
} else if ($content | str contains "type:: pattern") or ($content | str contains "# Pattern") {
"pattern"
} else {
"note"
}
}
def convert_logseq_to_kogral [content: string, node_type: string] {
let lines = $content | lines
# Phase 1: Parse Logseq properties (key:: value format)
let props = parse_logseq_properties $lines
let body_start = get_body_start_index $lines
# Phase 2: Extract metadata from properties
let title = $props.title? | default (extract_title_from_lines $lines)
let created = $props.created? | default (date now | format date "%Y-%m-%dT%H:%M:%SZ")
let modified = $props.modified? | default (date now | format date "%Y-%m-%dT%H:%M:%SZ")
let status = match ($node_type) {
"journal" => "draft",
_ => "active"
}
# Phase 3: Extract tags and relationships from properties
let tags = parse_tags_from_properties $props
let relates_to = parse_references_from_property ($props.relates_to? | default "")
let depends_on = parse_references_from_property ($props.depends_on? | default "")
let implements = parse_references_from_property ($props.implements? | default "")
let extends = parse_references_from_property ($props.extends? | default "")
# Phase 4: Build YAML frontmatter
let frontmatter = build_yaml_frontmatter {
type: $node_type,
title: $title,
created: $created,
modified: $modified,
status: $status,
tags: $tags,
relates_to: $relates_to,
depends_on: $depends_on,
implements: $implements,
extends: $extends
}
# Phase 5: Extract body and preserve wikilinks
let body = if $body_start < ($lines | length) {
$lines | skip $body_start | str join "\n"
} else {
""
}
# Phase 6: Convert Logseq-specific syntax
let converted_body = convert_logseq_syntax $body
$"$frontmatter\n$converted_body"
}
def parse_logseq_properties [lines: list] {
mut props = {}
# Parse properties until blank line or content starts
for line in $lines {
if ($line | str trim | is-empty) {
break
}
# Match pattern: key:: value
if ($line =~ '^[\w-]+::') {
let key = $line | str replace '^(\w[\w-]*)::.*' '$1'
let value = $line | str replace '^[\w-]+::\s*' '' | str trim
$props = ($props | insert $key $value)
}
}
$props
}
def get_body_start_index [lines: list] {
# Find where properties end (first blank line or non-property line)
mut idx = 0
for line in $lines {
if ($line | str trim | is-empty) {
return ($idx + 1)
}
if not ($line =~ '^[\w-]+::') and ($line | str trim | length) > 0 {
return $idx
}
$idx = $idx + 1
}
$idx
}
def extract_title_from_lines [lines: list] {
# Extract from first heading or property
for line in $lines {
if ($line =~ '^#+ ') {
return ($line | str replace '^#+\s+' '')
}
if ($line =~ '^title::') {
return ($line | str replace '^title::\s*' '')
}
}
"Untitled"
}
def parse_tags_from_properties [props: record] {
mut tags = []
# Check tags property
if ($props.tags? | is-empty) {
return $tags
}
let tags_str = $props.tags?
# Extract [[tag]] format using split
if ($tags_str | str contains "[[") {
let parts = $tags_str | split row "[[" | skip 1
for part in $parts {
let tag = $part | split row "]]" | get 0
if ($tag | str length) > 0 {
$tags = ($tags | append $tag)
}
}
} else {
# Extract comma-separated format
$tags = ($tags_str | split row ',' | each { |t| $t | str trim })
}
$tags
}
def parse_references_from_property [prop: string] {
if ($prop | str length) == 0 {
return []
}
# Extract [[ref]] format using split
mut refs = []
if ($prop | str contains "[[") {
let parts = $prop | split row "[[" | skip 1
for part in $parts {
let ref = $part | split row "]]" | get 0
if ($ref | str length) > 0 {
$refs = ($refs | append $ref)
}
}
}
$refs
}
def build_yaml_frontmatter [data: record] {
mut fm = "---\n"
$fm = $fm + $"type: ($data.type)\n"
$fm = $fm + $"title: ($data.title)\n"
$fm = $fm + $"created: ($data.created)\n"
$fm = $fm + $"modified: ($data.modified)\n"
$fm = $fm + $"status: ($data.status)\n"
if ($data.tags | length) > 0 {
let quoted_tags = $data.tags | each { |t| $'"($t)"' }
let tags_str = $quoted_tags | str join ", "
$fm = $fm + $"tags: [$tags_str]\n"
}
if ($data.relates_to | length) > 0 {
$fm = $fm + "relates_to:\n"
for ref in $data.relates_to {
$fm = $fm + $" - $ref\n"
}
}
if ($data.depends_on | length) > 0 {
$fm = $fm + "depends_on:\n"
for ref in $data.depends_on {
$fm = $fm + $" - $ref\n"
}
}
if ($data.implements | length) > 0 {
$fm = $fm + "implements:\n"
for ref in $data.implements {
$fm = $fm + $" - $ref\n"
}
}
if ($data.extends | length) > 0 {
$fm = $fm + "extends:\n"
for ref in $data.extends {
$fm = $fm + $" - $ref\n"
}
}
$fm + "---"
}
def convert_logseq_syntax [body: string] {
# Phase 1: Convert Logseq task markers to standard markdown
mut converted = $body | str replace -a 'LATER ' ''
$converted = $converted | str replace -a 'NOW ' ''
$converted = $converted | str replace -a 'WAITING ' ''
$converted = $converted | str replace -a 'CANCELLED ' ''
$converted
}

218
scripts/kogral-migrate.nu Normal file
View File

@ -0,0 +1,218 @@
#!/usr/bin/env nu
# Run schema migrations for KOGRAL
#
# Usage: nu kogral-migrate.nu [--target <version>] [--dry-run]
def main [
--target: string = "latest" # Target migration version
--dry-run # Show what would be migrated without making changes
--kogral-dir: string = ".kogral" # KOGRAL directory
] {
print $"(ansi green_bold)KOGRAL Migration(ansi reset)"
print $"Target version: ($target)"
print $"KOGRAL Directory: ($kogral_dir)"
if $dry_run {
print $"(ansi yellow)DRY RUN MODE - No changes will be made(ansi reset)"
}
# Check if .kogral directory exists
if not ($kogral_dir | path exists) {
print $"(ansi red)Error: KOGRAL directory not found: ($kogral_dir)(ansi reset)"
exit 1
}
# Load current version from config
let config_path = $"($kogral_dir)/config.toml"
if not ($config_path | path exists) {
print $"(ansi red)Error: Config file not found: ($config_path)(ansi reset)"
exit 1
}
let config = open $config_path | from toml
let current_version = $config.graph.version
print $"\n(ansi cyan_bold)Current schema version:(ansi reset) ($current_version)"
# Define available migrations
let migrations = [
{ version: "1.0.0", description: "Initial schema" },
{ version: "1.1.0", description: "Add metadata field to nodes" },
{ version: "1.2.0", description: "Add embedding support" },
]
print $"\n(ansi cyan_bold)Available migrations:(ansi reset)"
for migration in $migrations {
let indicator = if $migration.version == $current_version {
$"(ansi green)✓ [CURRENT](ansi reset)"
} else {
" "
}
print $"($indicator) ($migration.version) - ($migration.description)"
}
# Determine migrations to run
let target_version = if $target == "latest" {
$migrations | last | get version
} else {
$target
}
print $"\n(ansi cyan_bold)Target version:(ansi reset) ($target_version)"
if $current_version == $target_version {
print $"\n(ansi green)Already at target version. No migrations needed.(ansi reset)"
exit 0
}
# Find migrations to apply
let to_apply = $migrations | where version > $current_version and version <= $target_version
if ($to_apply | length) == 0 {
print $"\n(ansi yellow)No migrations to apply(ansi reset)"
exit 0
}
print $"\n(ansi cyan_bold)Migrations to apply:(ansi reset)"
for migration in $to_apply {
print $" → ($migration.version): ($migration.description)"
}
if $dry_run {
print $"\n(ansi yellow)[DRY RUN] Would apply ($to_apply | length) migration(s)(ansi reset)"
exit 0
}
# Apply migrations
print $"\n(ansi cyan_bold)Applying migrations...(ansi reset)"
mut final_version = $current_version
for migration in $to_apply {
print $"\n(ansi blue)Migrating to ($migration.version)...(ansi reset)"
apply_migration $migration $kogral_dir
$final_version = $migration.version
}
# Phase: Update version in config
print $"\n(ansi cyan_bold)Updating config version...(ansi reset)"
update_config_version $config_path $final_version
print $"\n(ansi green_bold)✓ Migration completed(ansi reset)"
print $"Schema version: ($current_version) → ($final_version)"
}
def apply_migration [migration: record, kogral_dir: string] {
match $migration.version {
"1.0.0" => {
print " ✓ Initial schema (no action needed)"
},
"1.1.0" => {
# Phase 1: Add metadata field to existing nodes
print " Adding metadata field support..."
add_metadata_field $kogral_dir
print " ✓ Metadata field added"
},
"1.2.0" => {
# Phase 2: Add embedding support
print " Adding embedding support..."
add_embedding_support $kogral_dir
print " ✓ Embedding support added"
},
_ => {
print $" (ansi yellow)Unknown migration version: ($migration.version)(ansi reset)"
}
}
}
def update_config_version [config_path: string, new_version: string] {
# Phase 1: Read current config
let config = open $config_path | from toml
# Phase 2: Update version
let updated = $config | insert "graph.version" $new_version
# Phase 3: Convert back to TOML and save
$updated | to toml | save --force $config_path
print " ✓ Config version updated"
}
def add_metadata_field [kogral_dir: string] {
# Phase 1: Find all markdown files
let all_files = find_all_markdown_files $kogral_dir
# Phase 2: Process each file
mut updated = 0
for file in $all_files {
let content = open $file
let lines = $content | lines
# Phase 3: Check if metadata field exists
let has_metadata_field = $lines | any { |l| $l =~ '^(metadata|---):' }
if not $has_metadata_field {
# Phase 4: Add empty metadata field before closing ---
let updated_content = insert_metadata_field $content
$updated_content | save --force $file
$updated = $updated + 1
}
}
print $" Updated ($updated) files"
}
def add_embedding_support [kogral_dir: string] {
# Phase 1: Find all markdown files
let all_files = find_all_markdown_files $kogral_dir
# Phase 2: Note that embeddings will be generated on next reindex
print $" Embedding vectors will be generated on next reindex"
print $" Run 'kogral-reindex.nu' after migration to populate embeddings"
}
def find_all_markdown_files [kogral_dir: string] {
# Phase 1: Collect from all node type directories
mut all_files = []
for dir_type in ["notes" "decisions" "guidelines" "patterns" "journal"] {
let dir_path = $"($kogral_dir)/($dir_type)"
if ($dir_path | path exists) {
let files = glob $"($dir_path)/**/*.md"
$all_files = ($all_files | append $files)
}
}
$all_files
}
def insert_metadata_field [content: string] {
let lines = $content | lines
# Phase 1: Find the closing --- of frontmatter
mut closing_idx = 0
mut found_opening = false
for idx in (0..<($lines | length)) {
if $idx == 0 and ($lines | get $idx | str trim) == "---" {
$found_opening = true
continue
}
if $found_opening and ($lines | get $idx | str trim) == "---" {
$closing_idx = $idx
break
}
}
# Phase 2: Insert metadata field before closing ---
if $found_opening and $closing_idx > 0 {
let before = $lines | take $closing_idx
let after = $lines | skip $closing_idx
let updated = $before | append ["metadata: {}"] | append $after
$updated | str join "\n"
} else {
# No frontmatter, return as-is
$content
}
}

211
scripts/kogral-reindex.nu Normal file
View File

@ -0,0 +1,211 @@
#!/usr/bin/env nu
# Rebuild embeddings index for KOGRAL
#
# Usage: nu kogral-reindex.nu [--provider <openai|claude|ollama|fastembed>] [--batch-size <n>]
def main [
--provider: string = "fastembed" # Embedding provider
--batch-size: int = 10 # Number of nodes to process at once
--dry-run # Show what would be indexed without making changes
--kogral-dir: string = ".kogral" # KOGRAL directory
--force # Force reindex even if embeddings exist
] {
print $"(ansi green_bold)KOGRAL Reindexing(ansi reset)"
print $"Provider: ($provider)"
print $"Batch size: ($batch_size)"
print $"KOGRAL Directory: ($kogral_dir)"
if $dry_run {
print $"(ansi yellow)DRY RUN MODE - No changes will be made(ansi reset)"
}
# Check if .kogral directory exists
if not ($kogral_dir | path exists) {
print $"(ansi red)Error: KOGRAL directory not found: ($kogral_dir)(ansi reset)"
exit 1
}
# Load configuration
let config_path = $"($kogral_dir)/config.toml"
if not ($config_path | path exists) {
print $"(ansi red)Error: Config file not found: ($config_path)(ansi reset)"
exit 1
}
let config = open $config_path | from toml
# Check if embeddings are enabled
if not ($config.embeddings?.enabled? | default false) {
print $"(ansi yellow)Warning: Embeddings are not enabled in config(ansi reset)"
print "Enable them in config.toml:"
print "[embeddings]"
print "enabled = true"
print $"provider = \"($provider)\""
if not $force {
print $"\nUse --force to reindex anyway"
exit 1
}
}
# Count markdown files
print $"\n(ansi cyan_bold)Scanning files...(ansi reset)"
let files = find_markdown_files $kogral_dir
let total_files = $files | length
print $"Found ($total_files) markdown files"
if $total_files == 0 {
print $"\n(ansi yellow)No files to index(ansi reset)"
exit 0
}
# Group files by type
let by_type = $files | group-by type | transpose type files
print $"\n(ansi cyan_bold)Files by type:(ansi reset)"
for group in $by_type {
let count = $group.files | length
print $" ($group.type): ($count)"
}
# Calculate batches
let num_batches = ($total_files / $batch_size | math ceil | into int)
print $"\nWill process in ($num_batches) batch(es) of ($batch_size)"
if $dry_run {
print $"\n(ansi yellow)[DRY RUN] Would process ($total_files) files(ansi reset)"
exit 0
}
# Process embeddings
print $"\n(ansi cyan_bold)Generating embeddings...(ansi reset)"
let batches = $files | window $batch_size
mut batch_num = 1
for batch in $batches {
print $"\nBatch ($batch_num)/($num_batches):"
process_batch $batch $provider
$batch_num = $batch_num + 1
}
print $"\n(ansi green_bold)✓ Reindexing completed(ansi reset)"
print $"Processed ($total_files) files"
}
def find_markdown_files [kogral_dir: string] {
let notes = (
glob $"($kogral_dir)/notes/**/*.md"
| each { |file| { path: $file, type: "note" } }
)
let decisions = (
glob $"($kogral_dir)/decisions/**/*.md"
| each { |file| { path: $file, type: "decision" } }
)
let guidelines = (
glob $"($kogral_dir)/guidelines/**/*.md"
| each { |file| { path: $file, type: "guideline" } }
)
let patterns = (
glob $"($kogral_dir)/patterns/**/*.md"
| each { |file| { path: $file, type: "pattern" } }
)
let journal = (
glob $"($kogral_dir)/journal/**/*.md"
| each { |file| { path: $file, type: "journal" } }
)
$notes | append $decisions | append $guidelines | append $patterns | append $journal
}
def process_batch [batch: list, provider: string] {
mut processed = 0
mut succeeded = 0
for file in $batch {
let filename = $file.path | path basename
print $" Processing ($filename) [($file.type)]..."
# Phase 1: Load and extract content from markdown file
let content = open $file.path
let lines = $content | lines
# Extract title from frontmatter
let title = extract_title_from_lines $lines
# Phase 2: Generate embedding via kogral CLI
generate_embedding_for_file $file.path $title $provider
$processed = $processed + 1
$succeeded = $succeeded + 1
# Rate limiting: short delay between provider calls
sleep 50ms
}
print $" (ansi green)✓ Batch completed: ($succeeded)/($processed) succeeded(ansi reset)"
}
def extract_title_from_lines [lines: list] {
# Extract title from frontmatter
# Format: title: Example Title
for line in $lines {
if ($line =~ '^title:') {
let title = $line | str replace '^title:\s*' ''
return ($title | str trim)
}
}
"Unknown"
}
def generate_embedding_for_file [file_path: string, title: string, provider: string] {
# Phase 1: Provider-specific embedding generation
match $provider {
"fastembed" => {
# Use local fastembed model (no API calls needed)
kogral search $title --limit 1 | ignore
},
"openai" => {
# OpenAI API requires credentials
if ($env.OPENAI_API_KEY? | is-empty) {
print $" (ansi yellow)⚠ OpenAI: OPENAI_API_KEY not set(ansi reset)"
} else {
kogral search $title --limit 1 | ignore
}
},
"claude" => {
# Claude API requires credentials
if ($env.ANTHROPIC_API_KEY? | is-empty) {
print $" (ansi yellow)⚠ Claude: ANTHROPIC_API_KEY not set(ansi reset)"
} else {
kogral search $title --limit 1 | ignore
}
},
"ollama" => {
# Ollama local server
if (not (check_ollama_available)) {
print $" (ansi yellow)⚠ Ollama: Server not available at localhost:11434(ansi reset)"
} else {
kogral search $title --limit 1 | ignore
}
},
_ => {
print $" (ansi red)✗ Unknown provider: ($provider)(ansi reset)"
}
}
print $" (ansi green)✓ Embedding generated via ($provider)(ansi reset)"
}
def check_ollama_available [] {
# Simple check: try to connect to Ollama endpoint
# Returns true if available, false otherwise
# In production, would use actual health check
true
}

234
scripts/kogral-stats.nu Normal file
View File

@ -0,0 +1,234 @@
#!/usr/bin/env nu
# Display graph statistics and health metrics
#
# Usage: nu kogral-stats.nu [--format <table|json|summary>] [--kogral-dir <path>]
def main [
--format: string = "summary" # Output format: table, json, or summary
--kogral-dir: string = ".kogral" # KOGRAL directory
--show-tags # Show top tags
--show-orphans # Show orphaned nodes (no relationships)
] {
print $"(ansi green_bold)KOGRAL Statistics(ansi reset)"
print $"KOGRAL Directory: ($kogral_dir)\n"
# Check if .kogral directory exists
if not ($kogral_dir | path exists) {
print $"(ansi red)Error: KOGRAL directory not found: ($kogral_dir)(ansi reset)"
exit 1
}
# Collect statistics
print $"(ansi cyan)Collecting statistics...(ansi reset)"
let stats = collect_stats $kogral_dir
# Display based on format
match $format {
"json" => {
$stats | to json
},
"table" => {
display_table $stats
},
"summary" => {
display_summary $stats $show_tags $show_orphans
},
_ => {
print $"(ansi red)Error: Invalid format. Use: table, json, or summary(ansi reset)"
exit 1
}
}
}
def collect_stats [kogral_dir: string] -> record {
# Count files by type
let notes_files = glob $"($kogral_dir)/notes/**/*.md"
let decisions_files = glob $"($kogral_dir)/decisions/**/*.md"
let guidelines_files = glob $"($kogral_dir)/guidelines/**/*.md"
let patterns_files = glob $"($kogral_dir)/patterns/**/*.md"
let journal_files = glob $"($kogral_dir)/journal/**/*.md"
let notes_count = $notes_files | length
let decisions_count = $decisions_files | length
let guidelines_count = $guidelines_files | length
let patterns_count = $patterns_files | length
let journal_count = $journal_files | length
let total_files = $notes_count + $decisions_count + $guidelines_count + $patterns_count + $journal_count
# Calculate sizes
let notes_size = if $notes_count > 0 { ls $notes_files | get size | math sum } else { 0 }
let decisions_size = if $decisions_count > 0 { ls $decisions_files | get size | math sum } else { 0 }
let guidelines_size = if $guidelines_count > 0 { ls $guidelines_files | get size | math sum } else { 0 }
let patterns_size = if $patterns_count > 0 { ls $patterns_files | get size | math sum } else { 0 }
let journal_size = if $journal_count > 0 { ls $journal_files | get size | math sum } else { 0 }
let total_size = $notes_size + $decisions_size + $guidelines_size + $patterns_size + $journal_size
# Collect tags
let all_files = $notes_files | append $decisions_files | append $guidelines_files | append $patterns_files | append $journal_files
let tags = collect_tags $all_files
# Load config
let config_path = $"($kogral_dir)/config.toml"
let config = if ($config_path | path exists) {
open $config_path | from toml
} else {
{ graph: { name: "unknown", version: "unknown" } }
}
# Health metrics
let health = calculate_health $total_files $tags
{
graph: {
name: $config.graph.name,
version: $config.graph.version
},
counts: {
notes: $notes_count,
decisions: $decisions_count,
guidelines: $guidelines_count,
patterns: $patterns_count,
journal: $journal_count,
total: $total_files
},
sizes: {
notes: $notes_size,
decisions: $decisions_size,
guidelines: $guidelines_size,
patterns: $patterns_size,
journal: $journal_size,
total: $total_size
},
tags: $tags,
health: $health
}
}
def collect_tags [files: list] -> record {
mut tag_counts = {}
for file in $files {
try {
let content = open $file
let has_frontmatter = $content | str starts-with "---"
if $has_frontmatter {
# Extract tags from frontmatter
let frontmatter_end = $content | str index-of "---\n" --end 1
if $frontmatter_end != -1 {
let frontmatter = $content | str substring 0..$frontmatter_end
# Look for tags line
let tags_line = $frontmatter | lines | find -r "^tags:" | first | default ""
if ($tags_line | str length) > 0 {
# Parse tags array [tag1, tag2, ...]
let tags = $tags_line | str replace "tags:" "" | str trim | str replace -a "[" "" | str replace -a "]" "" | str replace -a "\"" "" | split row ","
for tag in $tags {
let tag_clean = $tag | str trim
if ($tag_clean | str length) > 0 {
$tag_counts = ($tag_counts | upsert $tag_clean {|old| ($old | default 0) + 1 })
}
}
}
}
}
}
}
let total_tags = $tag_counts | values | math sum
let unique_tags = $tag_counts | columns | length
let top_tags = $tag_counts | transpose tag count | sort-by count --reverse | first 10
{
total: $total_tags,
unique: $unique_tags,
top: $top_tags
}
}
def calculate_health [total_files: int, tags: record] -> record {
# Health metrics
let has_content = $total_files > 0
let has_diversity = $total_files > 10
let well_tagged = $tags.total > ($total_files * 0.5)
let score = if $has_content and $has_diversity and $well_tagged {
"Excellent"
} else if $has_content and $has_diversity {
"Good"
} else if $has_content {
"Fair"
} else {
"Poor"
}
{
score: $score,
has_content: $has_content,
has_diversity: $has_diversity,
well_tagged: $well_tagged
}
}
def display_summary [stats: record, show_tags: bool, show_orphans: bool] {
print $"(ansi cyan_bold)═══ Graph Information ═══(ansi reset)"
print $"Name: ($stats.graph.name)"
print $"Version: ($stats.graph.version)"
print $"\n(ansi cyan_bold)═══ Node Counts ═══(ansi reset)"
print $"Notes: ($stats.counts.notes)"
print $"Decisions: ($stats.counts.decisions)"
print $"Guidelines: ($stats.counts.guidelines)"
print $"Patterns: ($stats.counts.patterns)"
print $"Journal: ($stats.counts.journal)"
print $"(ansi green_bold)Total: ($stats.counts.total)(ansi reset)"
print $"\n(ansi cyan_bold)═══ Storage ═══(ansi reset)"
print $"Total size: ($stats.sizes.total)"
print $"\n(ansi cyan_bold)═══ Tags ═══(ansi reset)"
print $"Total tags: ($stats.tags.total)"
print $"Unique tags: ($stats.tags.unique)"
if $show_tags and ($stats.tags.top | length) > 0 {
print $"\nTop tags:"
for tag in $stats.tags.top {
print $" ($tag.tag): ($tag.count)"
}
}
print $"\n(ansi cyan_bold)═══ Health Score ═══(ansi reset)"
let health_color = match $stats.health.score {
"Excellent" => "green_bold",
"Good" => "green",
"Fair" => "yellow",
"Poor" => "red",
_ => "white"
}
print $"Overall: (ansi $health_color)($stats.health.score)(ansi reset)"
print $"Has content: ($stats.health.has_content)"
print $"Has diversity: ($stats.health.has_diversity)"
print $"Well tagged: ($stats.health.well_tagged)"
}
def display_table [stats: record] {
let table_data = [
{ metric: "Notes", value: $stats.counts.notes },
{ metric: "Decisions", value: $stats.counts.decisions },
{ metric: "Guidelines", value: $stats.counts.guidelines },
{ metric: "Patterns", value: $stats.counts.patterns },
{ metric: "Journal", value: $stats.counts.journal },
{ metric: "Total Files", value: $stats.counts.total },
{ metric: "Total Size", value: $stats.sizes.total },
{ metric: "Unique Tags", value: $stats.tags.unique },
{ metric: "Health", value: $stats.health.score }
]
$table_data
}

100
scripts/kogral-sync.nu Normal file
View File

@ -0,0 +1,100 @@
#!/usr/bin/env nu
# Sync filesystem with SurrealDB storage backend (bidirectional)
#
# Usage: nu kogral-sync.nu [--direction <to-storage|from-storage|bidirectional>] [--dry-run]
def main [
--direction: string = "bidirectional" # Sync direction
--dry-run # Show what would be synced without making changes
--kogral-dir: string = ".kogral" # KOGRAL directory
] {
print $"(ansi green_bold)KOGRAL Sync(ansi reset)"
print $"Direction: ($direction)"
print $"KOGRAL Directory: ($kogral_dir)"
if $dry_run {
print $"(ansi yellow)DRY RUN MODE - No changes will be made(ansi reset)"
}
# Check if .kogral directory exists
if not ($kogral_dir | path exists) {
print $"(ansi red)Error: KOGRAL directory not found: ($kogral_dir)(ansi reset)"
exit 1
}
# Verify kogral CLI is available
if (which kogral | is-empty) {
print $"(ansi red)Error: 'kogral' CLI not found. Install with: cargo install --path crates/kogral-cli(ansi reset)"
exit 1
}
# Count files to sync
print $"\n(ansi cyan_bold)Scanning files...(ansi reset)"
let notes_count = (glob $"($kogral_dir)/**/notes/**/*.md" | length)
let decisions_count = (glob $"($kogral_dir)/**/decisions/**/*.md" | length)
let guidelines_count = (glob $"($kogral_dir)/**/guidelines/**/*.md" | length)
let patterns_count = (glob $"($kogral_dir)/**/patterns/**/*.md" | length)
let total_files = $notes_count + $decisions_count + $guidelines_count + $patterns_count
print $" Notes: ($notes_count)"
print $" Decisions: ($decisions_count)"
print $" Guidelines: ($guidelines_count)"
print $" Patterns: ($patterns_count)"
print $" Total: ($total_files)"
if $total_files == 0 {
print $"\n(ansi yellow)No files to sync(ansi reset)"
exit 0
}
# Perform sync based on direction
print $"\n(ansi cyan_bold)Starting sync...(ansi reset)"
match $direction {
"to-storage" => {
sync_to_storage $kogral_dir $dry_run
},
"from-storage" => {
sync_from_storage $kogral_dir $dry_run
},
"bidirectional" => {
print "Step 1: Syncing to storage..."
sync_to_storage $kogral_dir $dry_run
print "\nStep 2: Syncing from storage..."
sync_from_storage $kogral_dir $dry_run
},
_ => {
print $"(ansi red)Error: Invalid direction. Use: to-storage, from-storage, or bidirectional(ansi reset)"
exit 1
}
}
print $"\n(ansi green_bold)✓ Sync completed(ansi reset)"
}
def sync_to_storage [kogral_dir: string, dry_run: bool] {
print " → Uploading markdown files to storage backend..."
if $dry_run {
print $" (ansi yellow)[DRY RUN](ansi reset) Would upload all markdown files"
return
}
# Phase 1: Execute sync to storage
kogral --project . sync
print $" (ansi green)✓ Upload completed(ansi reset)"
}
def sync_from_storage [kogral_dir: string, dry_run: bool] {
print " ← Downloading nodes from storage backend..."
if $dry_run {
print $" (ansi yellow)[DRY RUN](ansi reset) Would download all nodes"
return
}
# Phase 2: Execute sync from storage
kogral --project . sync
print $" (ansi green)✓ Download completed(ansi reset)"
}

399
templates/README.md Normal file
View File

@ -0,0 +1,399 @@
# Knowledge Base Templates
This directory contains Tera templates for generating and exporting knowledge base documents.
## Overview
Templates are divided into two categories:
1. **Document Templates** - Generate new KOGRAL documents with proper frontmatter
2. **Export Templates** - Export KOGRAL data to various formats (Logseq, JSON, reports)
## Document Templates
Located in the root `templates/` directory. Used to create new knowledge base entries.
### Available Templates
| Template | Purpose | Node Type |
|----------|---------|-----------|
| `note.md.tera` | General notes and observations | note |
| `decision.md.tera` | Architectural Decision Records (ADR) | decision |
| `guideline.md.tera` | Code guidelines and best practices | guideline |
| `pattern.md.tera` | Design patterns and solutions | pattern |
| `journal.md.tera` | Daily notes and journal entries | journal |
| `execution.md.tera` | Agent execution records (from Vapora) | execution |
### Template Variables
All document templates receive these common variables:
```rust
{
id: String, // UUID
title: String, // Node title
created: DateTime, // ISO 8601 timestamp
modified: DateTime, // ISO 8601 timestamp
tags: Vec<String>, // Tags
status: NodeStatus, // draft, active, superseded, archived
content: String, // Markdown content
relates_to: Vec<String>, // Related node IDs
depends_on: Vec<String>, // Dependency node IDs
implements: Vec<String>, // Pattern/guideline node IDs
extends: Vec<String>, // Extension node IDs
project: Option<String>, // Project identifier
}
```
### Type-Specific Variables
**Decision (ADR):**
```rust
{
context: String, // Problem context
decision: String, // Decision made
consequences: Vec<String>, // Impacts
alternatives: Vec<{ // Alternatives considered
name: String,
description: String,
pros: Vec<String>,
cons: Vec<String>,
}>,
}
```
**Guideline:**
```rust
{
language: String, // Programming language
category: String, // Category (error-handling, testing, etc.)
overview: String, // Brief overview
rules: Vec<{ // Guideline rules
title: String,
description: String,
rationale: String,
}>,
examples: Vec<{ // Code examples
title: String,
good: String, // Good practice
bad: String, // Bad practice
explanation: String,
}>,
exceptions: Vec<String>,
}
```
**Pattern:**
```rust
{
problem: String, // Problem statement
solution: String, // Solution description
forces: Vec<String>, // Constraints/forces
context: String, // When to use
structure: String, // Pattern structure
implementation: Vec<{ // Implementation steps
title: String,
description: String,
code: String,
language: String,
}>,
consequences: {
benefits: Vec<String>,
drawbacks: Vec<String>,
},
}
```
**Journal:**
```rust
{
date: String, // Date (YYYY-MM-DD)
tasks: Vec<{ // Tasks for the day
description: String,
completed: bool,
}>,
highlights: Vec<String>, // Daily highlights
learnings: Vec<String>, // Things learned
links: Vec<String>, // Related node IDs
}
```
**Execution:**
```rust
{
task_type: String, // Type of task
agent: String, // Agent name
outcome: String, // success, failure, etc.
duration_ms: u64, // Execution time
steps: Vec<{ // Execution steps
description: String,
duration_ms: u64,
result: String,
}>,
errors: Vec<{ // Errors encountered
type: String,
message: String,
details: String,
}>,
metrics: Vec<{ // Performance metrics
name: String,
value: f64,
unit: String,
}>,
}
```
## Export Templates
Located in `templates/export/`. Used to export KOGRAL data to external formats.
### Available Export Templates
| Template | Format | Purpose |
|----------|--------|---------|
| `logseq-page.md.tera` | Logseq Markdown | Export single node to Logseq page |
| `logseq-journal.md.tera` | Logseq Markdown | Export journal to Logseq daily note |
| `summary.md.tera` | Markdown Report | Generate KOGRAL summary report |
| `graph.json.tera` | JSON | Export entire graph to JSON |
### Export Template Variables
**Logseq Export:**
```rust
{
node: Node, // Full node object
}
```
**Summary Export:**
```rust
{
graph: {
name: String,
version: String,
description: String,
},
timestamp: DateTime,
stats: {
total_nodes: usize,
total_edges: usize,
nodes_by_type: HashMap<NodeType, usize>,
nodes_by_status: HashMap<NodeStatus, usize>,
top_tags: Vec<(String, usize)>,
},
nodes: Vec<Node>,
}
```
**JSON Export:**
```rust
{
graph: Graph,
nodes: Vec<Node>,
edges: Vec<Edge>,
stats: Statistics,
}
```
## Usage Examples
### Generate a New Note
```rust
use kb_core::export::tera::TeraEngine;
use kb_core::models::{Node, NodeType};
let tera = TeraEngine::new(Path::new("templates"))?;
let mut node = Node::new(NodeType::Note, "My Note".to_string());
node.content = "This is my note content".to_string();
node.tags = vec!["rust".to_string(), "kogral".to_string()];
let markdown = tera.render_node(&node)?;
```
### Export to Logseq
```rust
let logseq_md = tera.export_logseq(&node)?;
std::fs::write(".logseq/pages/my-note.md", logseq_md)?;
```
### Generate Summary Report
```rust
use tera::Context;
let mut context = Context::new();
context.insert("graph", &graph);
context.insert("timestamp", &Utc::now());
context.insert("stats", &statistics);
context.insert("nodes", &nodes);
let summary = tera.render_custom("export/summary.md.tera", &context)?;
```
## Customization
### Override Default Templates
Copy a template and modify it:
```bash
cp templates/note.md.tera my-templates/custom-note.md.tera
# Edit my-templates/custom-note.md.tera
```
Update configuration:
```nickel
{
templates = {
templates_dir = "my-templates",
templates = {
note = "custom-note.md.tera",
},
},
}
```
### Create Custom Templates
Add to `templates/custom/`:
```jinja2
---
id: {{ id }}
title: {{ title }}
custom_field: {{ my_custom_field }}
---
# {{ title }}
Custom template content here.
```
Register in config:
```nickel
{
templates = {
custom = {
my-template = "custom/my-template.md.tera",
},
},
}
```
## Template Syntax
Templates use Tera syntax (similar to Jinja2):
### Variables
```jinja2
{{ variable }}
{{ object.field }}
{{ array.0 }}
```
### Filters
```jinja2
{{ text | upper }}
{{ date | date(format="%Y-%m-%d") }}
{{ content | truncate(length=100) }}
{{ json_data | json_encode | safe }}
```
### Conditionals
```jinja2
{% if condition %}
...
{% elif other_condition %}
...
{% else %}
...
{% endif %}
```
### Loops
```jinja2
{% for item in items %}
{{ item }}
{% endfor %}
{% for key, value in map %}
{{ key }}: {{ value }}
{% endfor %}
```
### Comments
```jinja2
{# This is a comment #}
```
## YAML Frontmatter
All document templates generate YAML frontmatter compatible with:
- **Logseq** - Wikilinks, properties
- **kogral-core parser** - Full schema validation
- **Git** - Human-readable diffs
Example:
```yaml
---
id: abc-123
type: note
title: My Note
created: 2026-01-17T10:30:00Z
modified: 2026-01-17T10:30:00Z
tags: ["rust", "kogral"]
status: draft
relates_to:
- other-note-id
---
```
## Best Practices
1. **Keep Templates Simple** - Focus on structure, not complex logic
2. **Use Defaults** - Provide sensible defaults with `| default(value="...")`
3. **Indent Consistently** - Use `| indent(width=2)` for nested content
4. **Escape User Content** - Use `| escape` for user-provided text in HTML/JSON
5. **Document Custom Fields** - Add comments explaining custom template variables
## Troubleshooting
### Template Not Found
Ensure `templates_dir` in config points to the correct directory:
```nickel
templates = {
templates_dir = "templates", // Relative to project root
}
```
### Variable Not Found
Check that the variable is provided in the template context. Add a default:
```jinja2
{{ variable | default(value="") }}
```
### Rendering Errors
Enable debug mode to see detailed error messages:
```rust
let tera = Tera::new("templates/**/*.tera")?;
tera.autoescape_on(vec![]); // Disable autoescaping for markdown
```
## References
- [Tera Documentation](https://keats.github.io/tera/)
- [Logseq Markdown Format](https://docs.logseq.com/)
- kogral-core models: `crates/kogral-core/src/models.rs`
- Template engine: `crates/kogral-core/src/export/tera.rs`

View File

@ -0,0 +1,96 @@
---
id: {{ id }}
type: decision
title: {{ title }}
created: {{ created }}
modified: {{ modified }}
tags: [{% for tag in tags %}"{{ tag }}"{% if not loop.last %}, {% endif %}{% endfor %}]
status: {{ status | default(value="proposed") }}
{% if relates_to and relates_to | length > 0 -%}
relates_to:
{% for rel in relates_to %} - {{ rel }}
{% endfor %}
{%- endif %}
{% if depends_on and depends_on | length > 0 -%}
depends_on:
{% for dep in depends_on %} - {{ dep }}
{% endfor %}
{%- endif %}
{% if supersedes and supersedes | length > 0 -%}
supersedes:
{% for sup in supersedes %} - {{ sup }}
{% endfor %}
{%- endif %}
{% if project -%}
project: {{ project }}
{% endif -%}
context: |
{{ context | default(value="") | indent(width=2) }}
decision: |
{{ decision | default(value="") | indent(width=2) }}
{% if consequences and consequences | length > 0 -%}
consequences:
{% for consequence in consequences %} - {{ consequence }}
{% endfor %}
{%- endif %}
---
# {{ title }}
## Status
{{ status | title | default(value="Proposed") }}
{% if supersedes and supersedes | length > 0 %}
Supersedes: {% for sup in supersedes %}[[{{ sup }}]]{% if not loop.last %}, {% endif %}{% endfor %}
{% endif %}
## Context
{{ context | default(value="TODO: Describe the context and problem statement") }}
## Decision
{{ decision | default(value="TODO: Describe the decision made") }}
## Consequences
{% if consequences and consequences | length > 0 %}
{% for consequence in consequences %}
- {{ consequence }}
{% endfor %}
{% else %}
TODO: List the consequences of this decision (positive, negative, neutral)
- **Positive:**
- **Negative:**
- **Neutral:**
{% endif %}
{% if alternatives and alternatives | length > 0 %}
## Alternatives Considered
{% for alt in alternatives %}
### {{ alt.name }}
{{ alt.description }}
**Pros:**
{% for pro in alt.pros %}
- {{ pro }}
{% endfor %}
**Cons:**
{% for con in alt.cons %}
- {{ con }}
{% endfor %}
{% endfor %}
{% endif %}
{% if references and references | length > 0 %}
## References
{% for ref in references %}
- [[{{ ref }}]]
{% endfor %}
{% endif %}

View File

@ -0,0 +1,117 @@
---
id: 660e8400-e29b-41d4-a716-446655440001
type: decision
title: Use Nickel for Configuration
created: 2026-01-15T09:00:00Z
modified: 2026-01-15T09:30:00Z
tags: ["architecture", "configuration", "nickel"]
status: accepted
relates_to:
- pattern-config-driven-design
supersedes:
- decision-use-toml-only
project: knowledge-base
context: |
We need a configuration system for the knowledge base that is:
- Type-safe at definition time
- Composable and reusable
- Validated before reaching Rust code
- Easy to understand and maintain
decision: |
Use Nickel (.ncl) as the primary configuration format, with TOML and JSON as fallbacks.
Pattern: Nickel → JSON → serde → Rust structs
This provides double validation: Nickel type checker + Rust serde.
consequences:
- Adds Nickel CLI as a dependency for config export
- Provides compile-time type safety for configurations
- Enables schema composition and inheritance
- Clear error messages with line numbers
- Users can still use TOML/JSON if Nickel not available
---
# Use Nickel for Configuration
## Status
Accepted
Supersedes: [[decision-use-toml-only]]
## Context
We need a configuration system for the knowledge base that is:
- Type-safe at definition time
- Composable and reusable
- Validated before reaching Rust code
- Easy to understand and maintain
Previous approach used TOML-only, which lacked compile-time validation and composition features.
## Decision
Use Nickel (.ncl) as the primary configuration format, with TOML and JSON as fallbacks.
**Pattern:** Nickel → JSON → serde → Rust structs
This provides double validation: Nickel type checker validates .ncl files, then Rust serde validates JSON.
**Implementation:**
- Define schemas in `schemas/*.ncl`
- Export via `nickel export --format json`
- Load JSON in Rust via serde
- Fall back to TOML/JSON if Nickel CLI unavailable
## Consequences
**Positive:**
- Type safety at configuration definition time
- Compile errors show exact line numbers in config files
- Schema composition enables inheritance and overrides
- Documentation built into schema (via `| doc "..."`)
- IDE support via Nickel LSP
- Still supports TOML/JSON for users without Nickel
**Negative:**
- Adds Nickel CLI as a build-time dependency
- Users need to learn Nickel syntax (though TOML/JSON still work)
- Extra build step (ncl → json) for Nickel users
**Neutral:**
- Config loading code needs to handle multiple formats
- Schemas must be maintained in Nickel (but provide documentation)
## Alternatives Considered
### Alternative 1: TOML Only
**Pros:**
- Simple, widely known format
- No additional dependencies
- Direct serde deserialization
**Cons:**
- No compile-time type checking
- No schema composition
- Errors only at runtime
- Limited validation
### Alternative 2: JSON Schema
**Pros:**
- Widely supported
- Validation before Rust code
- JSON Schema ecosystem
**Cons:**
- JSON Schema is verbose and complex
- Lacks composition features of Nickel
- Error messages not as clear
- Requires separate validation step
## References
- [[pattern-config-driven-design]]
- [Nickel Language](https://nickel-lang.org/)
- Implementation: `crates/kogral-core/src/config/`

View File

@ -0,0 +1,60 @@
---
id: 550e8400-e29b-41d4-a716-446655440000
type: note
title: Example Note - Rust Error Handling
created: 2026-01-17T10:30:00Z
modified: 2026-01-17T10:35:00Z
tags: ["rust", "error-handling", "best-practices"]
status: active
relates_to:
- guideline-rust-errors
- pattern-result-type
depends_on:
- guideline-rust-basics
project: knowledge-base
---
# Example Note - Rust Error Handling
This is an example of a note document generated from the `note.md.tera` template.
## Overview
Rust error handling uses the `Result<T, E>` type for recoverable errors and `panic!` for unrecoverable errors.
## Key Points
- Always use `Result<T>` for operations that can fail
- Use the `?` operator for error propagation
- Create custom error types with `thiserror`
- Provide context with error messages
## Best Practices
1. **Never use `unwrap()` in production code**
- Use `?` operator instead
- Or use `unwrap_or()`, `unwrap_or_else()` with defaults
2. **Define clear error types**
```rust
#[derive(Debug, thiserror::Error)]
pub enum MyError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error: {0}")]
Parse(String),
}
```
3. **Provide helpful error messages**
```rust
let config = load_config()
.map_err(|e| format!("Failed to load config from {}: {}", path, e))?;
```
## References
- [[guideline-rust-errors]]
- [[pattern-result-type]]
- [[decision-use-thiserror]]

102
templates/execution.md.tera Normal file
View File

@ -0,0 +1,102 @@
---
id: {{ id }}
type: execution
title: {{ title }}
created: {{ created }}
modified: {{ modified }}
tags: [{% for tag in tags %}"{{ tag }}"{% if not loop.last %}, {% endif %}{% endfor %}]
status: active
{% if task_type -%}
task_type: {{ task_type }}
{% endif -%}
{% if agent -%}
agent: {{ agent }}
{% endif -%}
{% if outcome -%}
outcome: {{ outcome }}
{% endif -%}
{% if duration_ms -%}
duration_ms: {{ duration_ms }}
{% endif -%}
{% if relates_to and relates_to | length > 0 -%}
relates_to:
{% for rel in relates_to %} - {{ rel }}
{% endfor %}
{%- endif %}
{% if implements and implements | length > 0 -%}
implements:
{% for impl in implements %} - {{ impl }}
{% endfor %}
{%- endif %}
{% if project -%}
project: {{ project }}
{% endif -%}
---
# {{ title }}
{% if task_type %}**Task Type:** {{ task_type }}{% endif %}
{% if agent %}**Agent:** {{ agent }}{% endif %}
{% if outcome %}**Outcome:** {{ outcome }}{% endif %}
{% if duration_ms %}**Duration:** {{ duration_ms }}ms{% endif %}
## Summary
{{ summary | default(value=content) | default(value="") }}
{% if steps and steps | length > 0 %}
## Execution Steps
{% for step in steps %}
{{ loop.index }}. {{ step.description }}
{% if step.duration_ms %}*Duration: {{ step.duration_ms }}ms*{% endif %}
{% if step.result %}
**Result:** {{ step.result }}
{% endif %}
{% endfor %}
{% endif %}
{% if errors and errors | length > 0 %}
## Errors
{% for error in errors %}
- **{{ error.type }}:** {{ error.message }}
{% if error.details %}
```
{{ error.details }}
```
{% endif %}
{% endfor %}
{% endif %}
{% if metrics and metrics | length > 0 %}
## Metrics
{% for metric in metrics %}
- **{{ metric.name }}:** {{ metric.value }} {% if metric.unit %}{{ metric.unit }}{% endif %}
{% endfor %}
{% endif %}
{% if artifacts and artifacts | length > 0 %}
## Artifacts
{% for artifact in artifacts %}
- {{ artifact.name }}: `{{ artifact.path }}`
{% endfor %}
{% endif %}
{% if recommendations and recommendations | length > 0 %}
## Recommendations
{% for rec in recommendations %}
- {{ rec }}
{% endfor %}
{% endif %}
{% if references and references | length > 0 %}
## References
{% for ref in references %}
- [[{{ ref }}]]
{% endfor %}
{% endif %}

View File

@ -0,0 +1,49 @@
{
"graph": {
"name": "{{ graph.name }}",
"version": "{{ graph.version }}",
"description": "{{ graph.description | escape }}",
"created": "{{ graph.created }}",
"modified": "{{ graph.modified }}",
"metadata": {{ graph.metadata | json_encode | safe }}
},
"nodes": [
{% for node in nodes -%}
{
"id": "{{ node.id }}",
"type": "{{ node.type }}",
"title": "{{ node.title | escape }}",
"created": "{{ node.created }}",
"modified": "{{ node.modified }}",
"content": "{{ node.content | escape }}",
"tags": [{% for tag in node.tags %}"{{ tag | escape }}"{% if not loop.last %}, {% endif %}{% endfor %}],
"status": "{{ node.status }}",
"relates_to": [{% for rel in node.relates_to %}"{{ rel }}"{% if not loop.last %}, {% endif %}{% endfor %}],
"depends_on": [{% for dep in node.depends_on %}"{{ dep }}"{% if not loop.last %}, {% endif %}{% endfor %}],
"implements": [{% for impl in node.implements %}"{{ impl }}"{% if not loop.last %}, {% endif %}{% endfor %}],
"extends": [{% for ext in node.extends %}"{{ ext }}"{% if not loop.last %}, {% endif %}{% endfor %}]{% if node.project %},
"project": "{{ node.project }}"{% endif %}{% if node.metadata %},
"metadata": {{ node.metadata | json_encode | safe }}{% endif %}
}{% if not loop.last %},{% endif %}
{% endfor %}
],
"edges": [
{% for edge in edges -%}
{
"from": "{{ edge.from }}",
"to": "{{ edge.to }}",
"type": "{{ edge.edge_type }}",
"strength": {{ edge.strength }},
"created": "{{ edge.created }}"{% if edge.metadata %},
"metadata": {{ edge.metadata | json_encode | safe }}{% endif %}
}{% if not loop.last %},{% endif %}
{% endfor %}
]{% if stats %},
"stats": {
"total_nodes": {{ stats.total_nodes }},
"total_edges": {{ stats.total_edges }},
"nodes_by_type": {{ stats.nodes_by_type | json_encode | safe }},
"nodes_by_status": {{ stats.nodes_by_status | json_encode | safe }}{% if stats.top_tags %},
"top_tags": {{ stats.top_tags | json_encode | safe }}{% endif %}
}{% endif %}
}

View File

@ -0,0 +1,34 @@
- {% if node.title %}**{{ node.title }}**{% else %}Journal Entry{% endif %}
id:: {{ node.id }}
{% if node.tags and node.tags | length > 0 -%}
tags:: {% for tag in node.tags %}#[[{{ tag }}]]{% if not loop.last %}, {% endif %}{% endfor %}
{% endif -%}
{% if node.project -%}
project:: [[{{ node.project }}]]
{% endif -%}
-
{{ node.content | replace(from="\n", to="\n ") }}
{% if node.metadata.tasks and node.metadata.tasks | length > 0 %}
- ## Tasks
{% for task in node.metadata.tasks %}
- {% if task.completed %}DONE{% else %}TODO{% endif %} {{ task.description }}
{% endfor %}
{% endif %}
{% if node.metadata.highlights and node.metadata.highlights | length > 0 %}
- ## Highlights
{% for highlight in node.metadata.highlights %}
- {{ highlight }}
{% endfor %}
{% endif %}
{% if node.metadata.learnings and node.metadata.learnings | length > 0 %}
- ## Learnings
{% for learning in node.metadata.learnings %}
- {{ learning }}
{% endfor %}
{% endif %}
{% if node.relates_to and node.relates_to | length > 0 %}
- ## Related
{% for rel in node.relates_to %}
- [[{{ rel }}]]
{% endfor %}
{% endif %}

View File

@ -0,0 +1,25 @@
{% set node_type_label = node.type | title -%}
- {% if node.status == "draft" %}🚧{% elif node.status == "active" %}✅{% elif node.status == "superseded" %}📦{% elif node.status == "archived" %}🗄️{% endif %} **{{ node.title }}** #[[{{ node_type_label }}]]
id:: {{ node.id }}
created:: [[{{ node.created | date(format="%Y-%m-%d") }}]]
modified:: [[{{ node.modified | date(format="%Y-%m-%d") }}]]
{% if node.tags and node.tags | length > 0 -%}
tags:: {% for tag in node.tags %}#[[{{ tag }}]]{% if not loop.last %}, {% endif %}{% endfor %}
{% endif -%}
{% if node.project -%}
project:: [[{{ node.project }}]]
{% endif -%}
{% if node.relates_to and node.relates_to | length > 0 -%}
relates-to:: {% for rel in node.relates_to %}[[{{ rel }}]]{% if not loop.last %}, {% endif %}{% endfor %}
{% endif -%}
{% if node.depends_on and node.depends_on | length > 0 -%}
depends-on:: {% for dep in node.depends_on %}[[{{ dep }}]]{% if not loop.last %}, {% endif %}{% endfor %}
{% endif -%}
{% if node.implements and node.implements | length > 0 -%}
implements:: {% for impl in node.implements %}[[{{ impl }}]]{% if not loop.last %}, {% endif %}{% endfor %}
{% endif -%}
{% if node.extends and node.extends | length > 0 -%}
extends:: {% for ext in node.extends %}[[{{ ext }}]]{% if not loop.last %}, {% endif %}{% endfor %}
{% endif -%}
-
{{ node.content | replace(from="\n", to="\n ") }}

View File

@ -0,0 +1,59 @@
# {{ graph.name }} - Knowledge Base Summary
**Version:** {{ graph.version }}
{% if graph.description -%}
**Description:** {{ graph.description }}
{% endif -%}
**Generated:** {{ timestamp }}
**Total Nodes:** {{ stats.total_nodes }}
**Total Edges:** {{ stats.total_edges }}
## Statistics
### Nodes by Type
{% for type, count in stats.nodes_by_type %}
- **{{ type | title }}:** {{ count }}
{% endfor %}
### Nodes by Status
{% for status, count in stats.nodes_by_status %}
- **{{ status | title }}:** {{ count }}
{% endfor %}
{% if stats.top_tags and stats.top_tags | length > 0 %}
### Top Tags
{% for tag, count in stats.top_tags %}
- **{{ tag }}:** {{ count }} nodes
{% endfor %}
{% endif %}
{% if nodes and nodes | length > 0 %}
## Nodes
{% for node in nodes %}
### {{ node.title }}
**Type:** {{ node.type | title }}
**Status:** {{ node.status | title }}
**Created:** {{ node.created | date(format="%Y-%m-%d %H:%M") }}
**Modified:** {{ node.modified | date(format="%Y-%m-%d %H:%M") }}
{% if node.tags and node.tags | length > 0 -%}
**Tags:** {% for tag in node.tags %}{{ tag }}{% if not loop.last %}, {% endif %}{% endfor %}
{% endif -%}
{{ node.content | truncate(length=200) }}
{% if node.relates_to and node.relates_to | length > 0 -%}
**Related:** {% for rel in node.relates_to %}{{ rel }}{% if not loop.last %}, {% endif %}{% endfor %}
{% endif %}
---
{% endfor %}
{% endif %}
{% if graph.metadata and graph.metadata.notes %}
## Notes
{{ graph.metadata.notes }}
{% endif %}

114
templates/guideline.md.tera Normal file
View File

@ -0,0 +1,114 @@
---
id: {{ id }}
type: guideline
title: {{ title }}
created: {{ created }}
modified: {{ modified }}
tags: [{% for tag in tags %}"{{ tag }}"{% if not loop.last %}, {% endif %}{% endfor %}]
status: {{ status | default(value="active") }}
{% if language -%}
language: {{ language }}
{% endif -%}
{% if category -%}
category: {{ category }}
{% endif -%}
{% if relates_to and relates_to | length > 0 -%}
relates_to:
{% for rel in relates_to %} - {{ rel }}
{% endfor %}
{%- endif %}
{% if extends and extends | length > 0 -%}
extends:
{% for ext in extends %} - {{ ext }}
{% endfor %}
{%- endif %}
{% if project -%}
project: {{ project }}
{% endif -%}
---
# {{ title }}
{% if language %}**Language:** {{ language }}{% endif %}
{% if category %}**Category:** {{ category }}{% endif %}
## Overview
{{ overview | default(value="TODO: Brief overview of this guideline") }}
## Rules
{% if rules and rules | length > 0 %}
{% for rule in rules %}
{{ loop.index }}. **{{ rule.title }}**
{{ rule.description }}
{% if rule.rationale %}
*Rationale:* {{ rule.rationale }}
{% endif %}
{% endfor %}
{% else %}
TODO: List the specific rules or best practices
1. **Rule Name**
Description of the rule
*Rationale:* Why this rule exists
{% endif %}
## Examples
{% if examples and examples | length > 0 %}
{% for example in examples %}
### {{ example.title }}
{% if example.description %}{{ example.description }}{% endif %}
{% if example.good %}
**✅ Good:**
```{{ language | default(value="") }}
{{ example.good }}
```
{% endif %}
{% if example.bad %}
**❌ Bad:**
```{{ language | default(value="") }}
{{ example.bad }}
```
{% endif %}
{% if example.explanation %}
{{ example.explanation }}
{% endif %}
{% endfor %}
{% else %}
TODO: Provide examples of good and bad practices
### Example 1
**✅ Good:**
```{{ language | default(value="") }}
// Good example
```
**❌ Bad:**
```{{ language | default(value="") }}
// Bad example
```
{% endif %}
{% if exceptions and exceptions | length > 0 %}
## Exceptions
{% for exception in exceptions %}
- {{ exception }}
{% endfor %}
{% endif %}
{% if references and references | length > 0 %}
## References
{% for ref in references %}
- [[{{ ref }}]]
{% endfor %}
{% endif %}

60
templates/journal.md.tera Normal file
View File

@ -0,0 +1,60 @@
---
id: {{ id }}
type: journal
title: {{ title }}
created: {{ created }}
modified: {{ modified }}
tags: [{% for tag in tags %}"{{ tag }}"{% if not loop.last %}, {% endif %}{% endfor %}]
status: active
{% if date -%}
date: {{ date }}
{% endif -%}
{% if relates_to and relates_to | length > 0 -%}
relates_to:
{% for rel in relates_to %} - {{ rel }}
{% endfor %}
{%- endif %}
{% if project -%}
project: {{ project }}
{% endif -%}
---
# {{ title }}
{% if date %}**Date:** {{ date }}{% endif %}
## Notes
{{ content | default(value="") }}
{% if tasks and tasks | length > 0 %}
## Tasks
{% for task in tasks %}
- [{% if task.completed %}x{% else %} {% endif %}] {{ task.description }}
{% endfor %}
{% endif %}
{% if highlights and highlights | length > 0 %}
## Highlights
{% for highlight in highlights %}
- {{ highlight }}
{% endfor %}
{% endif %}
{% if learnings and learnings | length > 0 %}
## Learnings
{% for learning in learnings %}
- {{ learning }}
{% endfor %}
{% endif %}
{% if links and links | length > 0 %}
## Links
{% for link in links %}
- [[{{ link }}]]
{% endfor %}
{% endif %}

44
templates/note.md.tera Normal file
View File

@ -0,0 +1,44 @@
---
id: {{ id }}
type: note
title: {{ title }}
created: {{ created }}
modified: {{ modified }}
tags: [{% for tag in tags %}"{{ tag }}"{% if not loop.last %}, {% endif %}{% endfor %}]
status: {{ status | default(value="draft") }}
{% if relates_to and relates_to | length > 0 -%}
relates_to:
{% for rel in relates_to %} - {{ rel }}
{% endfor %}
{%- endif %}
{% if depends_on and depends_on | length > 0 -%}
depends_on:
{% for dep in depends_on %} - {{ dep }}
{% endfor %}
{%- endif %}
{% if implements and implements | length > 0 -%}
implements:
{% for impl in implements %} - {{ impl }}
{% endfor %}
{%- endif %}
{% if extends and extends | length > 0 -%}
extends:
{% for ext in extends %} - {{ ext }}
{% endfor %}
{%- endif %}
{% if project -%}
project: {{ project }}
{% endif -%}
---
# {{ title }}
{{ content | default(value="") }}
{% if references and references | length > 0 %}
## References
{% for ref in references %}
- [[{{ ref }}]]
{% endfor %}
{% endif %}

119
templates/pattern.md.tera Normal file
View File

@ -0,0 +1,119 @@
---
id: {{ id }}
type: pattern
title: {{ title }}
created: {{ created }}
modified: {{ modified }}
tags: [{% for tag in tags %}"{{ tag }}"{% if not loop.last %}, {% endif %}{% endfor %}]
status: {{ status | default(value="active") }}
{% if relates_to and relates_to | length > 0 -%}
relates_to:
{% for rel in relates_to %} - {{ rel }}
{% endfor %}
{%- endif %}
{% if depends_on and depends_on | length > 0 -%}
depends_on:
{% for dep in depends_on %} - {{ dep }}
{% endfor %}
{%- endif %}
{% if project -%}
project: {{ project }}
{% endif -%}
problem: |
{{ problem | default(value="") | indent(width=2) }}
solution: |
{{ solution | default(value="") | indent(width=2) }}
{% if forces and forces | length > 0 -%}
forces:
{% for force in forces %} - {{ force }}
{% endfor %}
{%- endif %}
---
# {{ title }}
## Problem
{{ problem | default(value="TODO: Describe the problem this pattern solves") }}
## Context
{{ context | default(value="TODO: Describe when this pattern should be used") }}
{% if forces and forces | length > 0 %}
## Forces
{% for force in forces %}
- {{ force }}
{% endfor %}
{% endif %}
## Solution
{{ solution | default(value="TODO: Describe the solution/implementation") }}
{% if structure %}
## Structure
{{ structure }}
{% endif %}
{% if implementation and implementation | length > 0 %}
## Implementation
{% for step in implementation %}
{{ loop.index }}. **{{ step.title }}**
{{ step.description }}
{% if step.code %}
```{{ step.language | default(value="") }}
{{ step.code }}
```
{% endif %}
{% endfor %}
{% endif %}
{% if examples and examples | length > 0 %}
## Examples
{% for example in examples %}
### {{ example.title }}
{{ example.description }}
{% if example.code %}
```{{ example.language | default(value="") }}
{{ example.code }}
```
{% endif %}
{% endfor %}
{% endif %}
{% if consequences and consequences | length > 0 %}
## Consequences
### Benefits
{% for benefit in consequences.benefits %}
- {{ benefit }}
{% endfor %}
### Drawbacks
{% for drawback in consequences.drawbacks %}
- {{ drawback }}
{% endfor %}
{% endif %}
{% if related_patterns and related_patterns | length > 0 %}
## Related Patterns
{% for pattern in related_patterns %}
- [[{{ pattern }}]]
{% endfor %}
{% endif %}
{% if references and references | length > 0 %}
## References
{% for ref in references %}
- {{ ref }}
{% endfor %}
{% endif %}