From 9ea04852a8aca6477d67314097a01901ea752c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesu=CC=81s=20Pe=CC=81rez?= Date: Fri, 23 Jan 2026 16:12:50 +0000 Subject: [PATCH] chore: add schemas and just recipes --- justfile | 94 ++++++ justfiles/.kogral/config.dev.json | 0 justfiles/.kogral/config.prod.json | 0 justfiles/.kogral/config.test.json | 0 justfiles/49969output | 0 justfiles/build.just | 121 +++++++ justfiles/ci.just | 83 +++++ justfiles/dev.just | 123 ++++++++ justfiles/docs.just | 99 ++++++ justfiles/nickel.just | 184 +++++++++++ justfiles/scripts.just | 137 ++++++++ justfiles/test.just | 89 ++++++ schemas/README.md | 206 ++++++++++++ schemas/frontmatter.ncl | 180 +++++++++++ schemas/kogral-config.ncl | 387 +++++++++++++++++++++++ schemas/kogral/contracts.ncl | 268 ++++++++++++++++ schemas/kogral/defaults.ncl | 97 ++++++ schemas/kogral/helpers.ncl | 111 +++++++ schemas/kogral/modes/dev.ncl | 48 +++ schemas/kogral/modes/prod.ncl | 57 ++++ schemas/kogral/modes/test.ncl | 52 +++ schemas/surrealdb/blocks.surql | 139 +++++++++ schemas/types.ncl | 47 +++ scripts/kogral-backup.nu | 164 ++++++++++ scripts/kogral-export-logseq.nu | 337 ++++++++++++++++++++ scripts/kogral-import-logseq.nu | 388 +++++++++++++++++++++++ scripts/kogral-migrate.nu | 218 +++++++++++++ scripts/kogral-reindex.nu | 211 +++++++++++++ scripts/kogral-stats.nu | 234 ++++++++++++++ scripts/kogral-sync.nu | 100 ++++++ templates/README.md | 399 ++++++++++++++++++++++++ templates/decision.md.tera | 96 ++++++ templates/examples/decision-example.md | 117 +++++++ templates/examples/note-example.md | 60 ++++ templates/execution.md.tera | 102 ++++++ templates/export/graph.json.tera | 49 +++ templates/export/logseq-journal.md.tera | 34 ++ templates/export/logseq-page.md.tera | 25 ++ templates/export/summary.md.tera | 59 ++++ templates/guideline.md.tera | 114 +++++++ templates/journal.md.tera | 60 ++++ templates/note.md.tera | 44 +++ templates/pattern.md.tera | 119 +++++++ 43 files changed, 5452 insertions(+) create mode 100644 justfile create mode 100644 justfiles/.kogral/config.dev.json create mode 100644 justfiles/.kogral/config.prod.json create mode 100644 justfiles/.kogral/config.test.json create mode 100644 justfiles/49969output create mode 100644 justfiles/build.just create mode 100644 justfiles/ci.just create mode 100644 justfiles/dev.just create mode 100644 justfiles/docs.just create mode 100644 justfiles/nickel.just create mode 100644 justfiles/scripts.just create mode 100644 justfiles/test.just create mode 100644 schemas/README.md create mode 100644 schemas/frontmatter.ncl create mode 100644 schemas/kogral-config.ncl create mode 100644 schemas/kogral/contracts.ncl create mode 100644 schemas/kogral/defaults.ncl create mode 100644 schemas/kogral/helpers.ncl create mode 100644 schemas/kogral/modes/dev.ncl create mode 100644 schemas/kogral/modes/prod.ncl create mode 100644 schemas/kogral/modes/test.ncl create mode 100644 schemas/surrealdb/blocks.surql create mode 100644 schemas/types.ncl create mode 100644 scripts/kogral-backup.nu create mode 100644 scripts/kogral-export-logseq.nu create mode 100644 scripts/kogral-import-logseq.nu create mode 100644 scripts/kogral-migrate.nu create mode 100644 scripts/kogral-reindex.nu create mode 100644 scripts/kogral-stats.nu create mode 100644 scripts/kogral-sync.nu create mode 100644 templates/README.md create mode 100644 templates/decision.md.tera create mode 100644 templates/examples/decision-example.md create mode 100644 templates/examples/note-example.md create mode 100644 templates/execution.md.tera create mode 100644 templates/export/graph.json.tera create mode 100644 templates/export/logseq-journal.md.tera create mode 100644 templates/export/logseq-page.md.tera create mode 100644 templates/export/summary.md.tera create mode 100644 templates/guideline.md.tera create mode 100644 templates/journal.md.tera create mode 100644 templates/note.md.tera create mode 100644 templates/pattern.md.tera diff --git a/justfile b/justfile new file mode 100644 index 0000000..cdd1077 --- /dev/null +++ b/justfile @@ -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 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 diff --git a/justfiles/.kogral/config.dev.json b/justfiles/.kogral/config.dev.json new file mode 100644 index 0000000..e69de29 diff --git a/justfiles/.kogral/config.prod.json b/justfiles/.kogral/config.prod.json new file mode 100644 index 0000000..e69de29 diff --git a/justfiles/.kogral/config.test.json b/justfiles/.kogral/config.test.json new file mode 100644 index 0000000..e69de29 diff --git a/justfiles/49969output b/justfiles/49969output new file mode 100644 index 0000000..e69de29 diff --git a/justfiles/build.just b/justfiles/build.just new file mode 100644 index 0000000..1c20b4d --- /dev/null +++ b/justfiles/build.just @@ -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" diff --git a/justfiles/ci.just b/justfiles/ci.just new file mode 100644 index 0000000..f3c31a1 --- /dev/null +++ b/justfiles/ci.just @@ -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" diff --git a/justfiles/dev.just b/justfiles/dev.just new file mode 100644 index 0000000..1c70da5 --- /dev/null +++ b/justfiles/dev.just @@ -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 diff --git a/justfiles/docs.just b/justfiles/docs.just new file mode 100644 index 0000000..6500094 --- /dev/null +++ b/justfiles/docs.just @@ -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" diff --git a/justfiles/nickel.just b/justfiles/nickel.just new file mode 100644 index 0000000..6a5c8f1 --- /dev/null +++ b/justfiles/nickel.just @@ -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" diff --git a/justfiles/scripts.just b/justfiles/scripts.just new file mode 100644 index 0000000..8d53c30 --- /dev/null +++ b/justfiles/scripts.just @@ -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 diff --git a/justfiles/test.just b/justfiles/test.just new file mode 100644 index 0000000..1ac746f --- /dev/null +++ b/justfiles/test.just @@ -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 diff --git a/schemas/README.md b/schemas/README.md new file mode 100644 index 0000000..1ee12bd --- /dev/null +++ b/schemas/README.md @@ -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` diff --git a/schemas/frontmatter.ncl b/schemas/frontmatter.ncl new file mode 100644 index 0000000..fa18841 --- /dev/null +++ b/schemas/frontmatter.ncl @@ -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: +# --- +# +# --- +# +# + +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, + }, +} diff --git a/schemas/kogral-config.ncl b/schemas/kogral-config.ncl new file mode 100644 index 0000000..d96488a --- /dev/null +++ b/schemas/kogral-config.ncl @@ -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 = {}, + }, +} diff --git a/schemas/kogral/contracts.ncl b/schemas/kogral/contracts.ncl new file mode 100644 index 0000000..07b7f28 --- /dev/null +++ b/schemas/kogral/contracts.ncl @@ -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 = {}, + }, +} diff --git a/schemas/kogral/defaults.ncl b/schemas/kogral/defaults.ncl new file mode 100644 index 0000000..2ae3033 --- /dev/null +++ b/schemas/kogral/defaults.ncl @@ -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, +} diff --git a/schemas/kogral/helpers.ncl b/schemas/kogral/helpers.ncl new file mode 100644 index 0000000..8f6fe39 --- /dev/null +++ b/schemas/kogral/helpers.ncl @@ -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, +} diff --git a/schemas/kogral/modes/dev.ncl b/schemas/kogral/modes/dev.ncl new file mode 100644 index 0000000..727fa82 --- /dev/null +++ b/schemas/kogral/modes/dev.ncl @@ -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, + }, + }, +} diff --git a/schemas/kogral/modes/prod.ncl b/schemas/kogral/modes/prod.ncl new file mode 100644 index 0000000..adaa4f9 --- /dev/null +++ b/schemas/kogral/modes/prod.ncl @@ -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, + }, + }, +} diff --git a/schemas/kogral/modes/test.ncl b/schemas/kogral/modes/test.ncl new file mode 100644 index 0000000..7703ce3 --- /dev/null +++ b/schemas/kogral/modes/test.ncl @@ -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, + }, + }, +} diff --git a/schemas/surrealdb/blocks.surql b/schemas/surrealdb/blocks.surql new file mode 100644 index 0000000..7971cd6 --- /dev/null +++ b/schemas/surrealdb/blocks.surql @@ -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; +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; +DEFINE FIELD properties.custom ON block TYPE option; +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); diff --git a/schemas/types.ncl b/schemas/types.ncl new file mode 100644 index 0000000..2dd57d3 --- /dev/null +++ b/schemas/types.ncl @@ -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, +} diff --git a/scripts/kogral-backup.nu b/scripts/kogral-backup.nu new file mode 100644 index 0000000..408036b --- /dev/null +++ b/scripts/kogral-backup.nu @@ -0,0 +1,164 @@ +#!/usr/bin/env nu +# Backup KOGRAL graphs to archive +# +# Usage: nu kogral-backup.nu [--output ] [--format ] [--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" + } +} diff --git a/scripts/kogral-export-logseq.nu b/scripts/kogral-export-logseq.nu new file mode 100644 index 0000000..d6f1fb9 --- /dev/null +++ b/scripts/kogral-export-logseq.nu @@ -0,0 +1,337 @@ +#!/usr/bin/env nu +# Export KOGRAL to Logseq format +# +# Usage: nu kogral-export-logseq.nu [--kogral-dir ] [--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)" +} diff --git a/scripts/kogral-import-logseq.nu b/scripts/kogral-import-logseq.nu new file mode 100644 index 0000000..fc9d0e0 --- /dev/null +++ b/scripts/kogral-import-logseq.nu @@ -0,0 +1,388 @@ +#!/usr/bin/env nu +# Import from Logseq graph to KOGRAL +# +# Usage: nu kogral-import-logseq.nu [--kogral-dir ] [--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 +} diff --git a/scripts/kogral-migrate.nu b/scripts/kogral-migrate.nu new file mode 100644 index 0000000..1030575 --- /dev/null +++ b/scripts/kogral-migrate.nu @@ -0,0 +1,218 @@ +#!/usr/bin/env nu +# Run schema migrations for KOGRAL +# +# Usage: nu kogral-migrate.nu [--target ] [--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 + } +} diff --git a/scripts/kogral-reindex.nu b/scripts/kogral-reindex.nu new file mode 100644 index 0000000..023da80 --- /dev/null +++ b/scripts/kogral-reindex.nu @@ -0,0 +1,211 @@ +#!/usr/bin/env nu +# Rebuild embeddings index for KOGRAL +# +# Usage: nu kogral-reindex.nu [--provider ] [--batch-size ] + +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 +} diff --git a/scripts/kogral-stats.nu b/scripts/kogral-stats.nu new file mode 100644 index 0000000..13ad9e9 --- /dev/null +++ b/scripts/kogral-stats.nu @@ -0,0 +1,234 @@ +#!/usr/bin/env nu +# Display graph statistics and health metrics +# +# Usage: nu kogral-stats.nu [--format ] [--kogral-dir ] + +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 +} diff --git a/scripts/kogral-sync.nu b/scripts/kogral-sync.nu new file mode 100644 index 0000000..68fae6b --- /dev/null +++ b/scripts/kogral-sync.nu @@ -0,0 +1,100 @@ +#!/usr/bin/env nu +# Sync filesystem with SurrealDB storage backend (bidirectional) +# +# Usage: nu kogral-sync.nu [--direction ] [--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)" +} diff --git a/templates/README.md b/templates/README.md new file mode 100644 index 0000000..b5c1b49 --- /dev/null +++ b/templates/README.md @@ -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, // Tags + status: NodeStatus, // draft, active, superseded, archived + content: String, // Markdown content + relates_to: Vec, // Related node IDs + depends_on: Vec, // Dependency node IDs + implements: Vec, // Pattern/guideline node IDs + extends: Vec, // Extension node IDs + project: Option, // Project identifier +} +``` + +### Type-Specific Variables + +**Decision (ADR):** +```rust +{ + context: String, // Problem context + decision: String, // Decision made + consequences: Vec, // Impacts + alternatives: Vec<{ // Alternatives considered + name: String, + description: String, + pros: Vec, + cons: Vec, + }>, +} +``` + +**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, +} +``` + +**Pattern:** +```rust +{ + problem: String, // Problem statement + solution: String, // Solution description + forces: Vec, // 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, + drawbacks: Vec, + }, +} +``` + +**Journal:** +```rust +{ + date: String, // Date (YYYY-MM-DD) + tasks: Vec<{ // Tasks for the day + description: String, + completed: bool, + }>, + highlights: Vec, // Daily highlights + learnings: Vec, // Things learned + links: Vec, // 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, + nodes_by_status: HashMap, + top_tags: Vec<(String, usize)>, + }, + nodes: Vec, +} +``` + +**JSON Export:** +```rust +{ + graph: Graph, + nodes: Vec, + edges: Vec, + 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` diff --git a/templates/decision.md.tera b/templates/decision.md.tera new file mode 100644 index 0000000..2d893ec --- /dev/null +++ b/templates/decision.md.tera @@ -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 %} diff --git a/templates/examples/decision-example.md b/templates/examples/decision-example.md new file mode 100644 index 0000000..c61019b --- /dev/null +++ b/templates/examples/decision-example.md @@ -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/` diff --git a/templates/examples/note-example.md b/templates/examples/note-example.md new file mode 100644 index 0000000..dc246b2 --- /dev/null +++ b/templates/examples/note-example.md @@ -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` type for recoverable errors and `panic!` for unrecoverable errors. + +## Key Points + +- Always use `Result` 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]] diff --git a/templates/execution.md.tera b/templates/execution.md.tera new file mode 100644 index 0000000..21fbb9d --- /dev/null +++ b/templates/execution.md.tera @@ -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 %} diff --git a/templates/export/graph.json.tera b/templates/export/graph.json.tera new file mode 100644 index 0000000..523780e --- /dev/null +++ b/templates/export/graph.json.tera @@ -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 %} +} diff --git a/templates/export/logseq-journal.md.tera b/templates/export/logseq-journal.md.tera new file mode 100644 index 0000000..e5b2e88 --- /dev/null +++ b/templates/export/logseq-journal.md.tera @@ -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 %} diff --git a/templates/export/logseq-page.md.tera b/templates/export/logseq-page.md.tera new file mode 100644 index 0000000..bd95245 --- /dev/null +++ b/templates/export/logseq-page.md.tera @@ -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 ") }} diff --git a/templates/export/summary.md.tera b/templates/export/summary.md.tera new file mode 100644 index 0000000..e2ba08d --- /dev/null +++ b/templates/export/summary.md.tera @@ -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 %} diff --git a/templates/guideline.md.tera b/templates/guideline.md.tera new file mode 100644 index 0000000..3c86b4b --- /dev/null +++ b/templates/guideline.md.tera @@ -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 %} diff --git a/templates/journal.md.tera b/templates/journal.md.tera new file mode 100644 index 0000000..4039099 --- /dev/null +++ b/templates/journal.md.tera @@ -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 %} diff --git a/templates/note.md.tera b/templates/note.md.tera new file mode 100644 index 0000000..0eb8a2f --- /dev/null +++ b/templates/note.md.tera @@ -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 %} diff --git a/templates/pattern.md.tera b/templates/pattern.md.tera new file mode 100644 index 0000000..b30405d --- /dev/null +++ b/templates/pattern.md.tera @@ -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 %}