loam

package module
v0.10.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 21, 2026 License: AGPL-3.0 Imports: 5 Imported by: 2

README

Loam 🌱

An Embedded Reactive & Transactional Engine for Content & Metadata.

Go Report Card Go Doc License Release

Loam é uma engine embutida de documentos desenhada para persistência transacional de conteúdo e metadados.

Por padrão, utiliza o Sistema de Arquivos + Git como banco de dados (.md, .yaml, .json, .csv), oferecendo controle de versão nativo e legibilidade humana. Sua arquitetura é desacoplada, permitindo a evolução para diferentes backends sem alterar a lógica da aplicação.

É ideal para toolmakers que constroem:

  • Assistentes de PKM (Obsidian, Logseq) - Storage layer apenas.
  • Gerenciadores de Configuração (GitOps, Dotfiles).
  • Pipelines de Dados Locais (ETL de CSV/JSON).
  • Geradores de Sites Estáticos (Hugo, Jekyll).

🗺️ Navegação

🤔 Por que Loam?

  • Local-First: Seus dados são arquivos de texto simples. Você mantém controle total e soberania sem depender da engine para acessá-los.
  • Histórico Nativo: Todo Save gera um rastro auditável no Git. Gerencie versões e correções com a mesma segurança de um repositório de código.
  • Integridade: Transações em lote e file-locking garantem que automações e scripts nunca corrompam o estado do cofre.
  • Reatividade: Reaja a mudanças externas em tempo real, integrando perfeitamente fluxos locais com sua aplicação.

📄 Arquivos Suportados (Smart Persistence)

O Adapter padrão (FS) detecta automaticamente o formato do arquivo baseado na extensão do ID, suportando leitura e escrita raw (--raw):

  • Markdown (.md): Padrão. Conteúdo + Frontmatter YAML.
  • JSON (.json): Serializa como objeto JSON puro. Campo content é opcional.
  • YAML (.yaml): Serializa como objeto YAML puro. Campo content é opcional.
  • CSV (.csv): Serializa como linha de valores. Suporta coleções com múltiplos documentos.

Smart Retrieval: Na leitura (Get), se o ID não tiver extensão (ex: dados), o Loam procura automaticamente por dados.md, dados.json, etc., respeitando a existência do arquivo.

🚀 Instalação

Via Go Install (Recomendado)
go install github.com/aretw0/loam/cmd/loam@latest
Via Release

Baixe os binários pré-compilados na página de Releases.

Compilando do Fonte (Build)

Para desenvolvedores, utilizamos make para simplificar o processo:

# Build para sua plataforma atual
make build

# Cross-compilation (Linux, Windows, Mac)
make cross-build

# Instalar localmente
make install
Executando Testes

Para rodar a suíte de testes (excluindo testes de stress que podem ser lentos no Windows):

# Windows (PowerShell)
go test -v ./pkg/... ./cmd/... ./internal/... ./tests/e2e ./tests/reactivity ./tests/typed

# Linux/Mac (via Makefile)
make test-fast

🛠️ CLI: Uso Básico

O Loam CLI funciona como um "Gerenciador de Conteúdo", abstraindo a persistência.

Inicializar

Inicia um cofre Loam. Por padrão usa o adapter de sistema de arquivos (FS + Git).

loam init
# Ou explicitamente:
loam init --adapter fs
Criar/Editar Documento

Salva conteúdo e registra a razão da mudança (Commits no caso do Git).

# Modo Simples (apenas mensagem)
loam write -id daily/2025-12-06 -content "Hoje foi um dia produtivo." -m "log diário"

# Modo Semântico (Type, Scope, Body)
loam write -id feature/nova-ideia -content "..." --type feat --scope ideias -m "adiciona rascunho"

# Modo Imperativo (--set)
# Define metadados individuais sem precisar de JSON
loam write --id docs/readme.md --content "Texto" --set title="Novo Readme" --set status=draft

# Modo Declarativo (--raw)
# Envie o documento inteiro via pipe. O Loam detecta Frontmatter/JSON/CSV.
echo '{"title":"Logs", "content":"..."}' | loam write --id logs/1.json --raw

[!NOTE] No modo --raw, se o ID não possuir extensão (ex: --id nota), a CLI assumirá .md por padrão para tentar parsear o conteúdo. Se estiver enviando JSON ou CSV sem extensão no ID, o parse falhará.

Sincronizar (Sync)

Sincroniza o cofre com o remoto configurado (se o adapter suportar).

loam sync
Outros Comandos
  • Ler: loam read -id daily/2025-12-06
  • Listar: loam list
  • Deletar: loam delete -id daily/2025-12-06

📦 Library: Uso em Go

Você pode embutir o Loam em seus próprios projetos Go para gerenciar persistência de dados.

go get github.com/aretw0/loam
Exemplo
package main

import (
 "context"
 "fmt"
 "log/slog"
 "os"

 "github.com/aretw0/loam/pkg/core"
 "github.com/aretw0/loam"
)

func main() {
 // 1. Inicializar Serviço (Factory) com Functional Options.
 // O primeiro argumento é a URI ou Path do cofre. Para o adapter FS, use o caminho do diretório.
 service, err := loam.New("./meus-docs",
  loam.WithAdapter("fs"), // Padrão
  loam.WithAutoInit(true), // Cria diretório e git init se necessário
  loam.WithLogger(slog.New(slog.NewTextHandler(os.Stdout, nil))),
 )
 if err != nil {
  panic(err)
 }

 // NOTA DE SEGURANÇA (Dev Experience):
 // Ao rodar via "go run" (Dev Mode), o Loam isola escritas em um diretório temporário para proteger seus dados.
 // Para ferramentas que apenas lêem (como CLIs de análise), use WithReadOnly(true) para acessar os arquivos reais com segurança:
 //
 // service, err := loam.New(".", loam.WithReadOnly(true)) // Bypass Sandbox (Read-Only)


 ctx := context.Background()

 // 2. Escrever (Save)
 // Salvamos o conteúdo com uma "razão de mudança" (Commit Message)
 // Isso garante que toda mudança tenha um porquê.
 ctxMsg := context.WithValue(ctx, core.ChangeReasonKey, "documento inicial")
 err = service.SaveDocument(ctxMsg, "daily/hoje", "# Dia Incrível\nComeçamos o projeto.", nil)
 if err != nil {
  panic(err)
 }
 fmt.Println("Documento salvo com sucesso!")

 // 3. Ler (Read)
 doc, err := service.GetDocument(ctx, "daily/hoje")
 if err != nil { // Tratamento simplificado
  panic(err)
 }
 fmt.Printf("Conteúdo recuperado:\n%s\n", doc.Content)

 // ... (veja exemplos completos em examples/basics/crud)
}
Typed Retrieval (Generics)

Para maior segurança de tipos, você pode usar o wrapper genérico:

type User struct { Name string `json:"name"` }

// Abre um repositório já tipado (leitura/escrita de User)
// O ID do documento é preservado, mas o conteúdo é mapeado para User.
userRepo, err := loam.OpenTypedRepository[User]("./meus-docs")
if err != nil {
    panic(err)
}

// Acesso tipado
user, _ := userRepo.Get(ctx, "users/alice")
fmt.Println(user.Data.Name) // Type-safe!
Reactivity (Watch)

Você pode observar mudanças em repositórios tipados para implementar "Hot Reload" de configurações ou interfaces reativas:

// Retorna um canal de core.Event
// Opcional: Use WithWatcherErrorHandler para capturar falhas de acesso a arquivos durante o monitoramento.
events, err := userRepo.Watch(ctx, "users/*", loam.WithWatcherErrorHandler(func(err error) {
    fmt.Printf("Erro no watcher: %v\n", err)
}))

go func() {
    for event := range events {
        fmt.Printf("Mudança detectada em %s\n", event.ID)
        // Recarregue o documento tipado se necessário
        newUser, _ := userRepo.Get(ctx, event.ID)
    }
}()

📂 Exemplos e Receitas

Demos (Funcionalidades do Core)

📚 Ver todos os exemplos e receitas (incluindo Calendar, Ledger, e automações avançadas).

Recipes (Casos de Uso)

📚 Documentação Técnica

Tuning de Performance

Se sua aplicação lida com rajadas massivas de eventos (ex: git checkout em repositórios enormes) e você nota que o watcher "congela", pode ser necessário aumentar o buffer de eventos para evitar bloqueios:

// Aumenta o buffer para 1000 eventos (Padrão: 100)
srv, _ := loam.New("path/to/vault", loam.WithEventBuffer(1000))

Known Issues

Linux/inotify
  • Devido a limitações do inotify, novos diretórios criados após o início do watcher não são monitorados automaticamente (é necessário reiniciar o processo ou recriar o watcher). Em Windows e macOS, isso geralmente funciona nativamente.
  • Repositórios muito grandes (milhares de diretórios) podem exceder o limite de file descriptors. Aumente o limite via sysctl fs.inotify.max_user_watches se necessário.
CSV & Nested Data
  • O Loam agora suporta Smart CSV, que detecta estruturas JSON aninhadas (map, []interface{}) e as preserva automaticamente.
  • Caveat (False Positives): Strings que parecem JSON (ex: "{foo}") podem ser interpretadas como objetos se não estiverem escapadas (ex: "\"{foo}\""). Em casos de ambiguidade, o parser favorece a estrutura.
  • Concorrência: A escrita em coleções (CSV) não possui locking de arquivo (flock). O uso concorrente por múltiplos processos pode resultar em perda de dados (Race Condition no ciclo Read-Modify-Write).
Strict Mode & Large Integers
  • O modo estrito (strict: true) preserva inteiros grandes usando json.Number.
  • Limitação de Performance: Ocorre uma pequena penalidade de performance devido à normalização recursiva necessária para garantir que formatos como YAML e Markdown comportem-se identicamente ao JSON.
  • Recomendação: Use strict: true se sua aplicação depende fortemente de IDs numéricos de 64 bits ou precisão decimal exata em metadados aninhados.

Status

🚧 Alpha. A API Go (github.com/aretw0/loam) e a CLI são estáveis para uso diário (Unix Compliant).

Licença

AGPL-3.0

Documentation

Overview

Package loam is the Composition Root for the Loam application.

It connects the core business logic (Domain Layer) with the infrastructure adapters (Persistence Layer) using the Hexagonal Architecture pattern.

Philosophy:

Loam is an "Embedded Transactional Engine" for content & metadata. It treats a collection of documents as a transactional database, abstracting the underlying storage mechanism. While the default implementation uses the File System and Git, Loam's core is agnostic, allowing for future adapters (e.g., S3, SQLite).

Features:

  • **Hexagonal Architecture**: Core domain is isolated from persistence details.
  • **Transactional Safe**: Atomic operations regardless of the underlying storage.
  • **Metadata First**: Native support for structured metadata indexing (Frontmatter, JSON fields, etc).
  • **Typed Retrieval**: Generic wrapper (`OpenTypedRepository[T]`) for type-safe document access.
  • **Default Adapter (FS + Git)**: Out-of-the-box support for local Markdown files with Git versioning.
  • **Extensible**: Designed to support other backends (SQL, S3, NoSQL) via `core.Repository`.

Usage:

// Initialize service with functional options
svc, err := loam.New("./vault",
	loam.WithAutoInit(true),
	loam.WithLogger(logger),
)

// Save a document with a change reason (semantics)
ctx := context.WithValue(context.Background(), core.ChangeReasonKey, "initial check-in")
err := svc.SaveDocument(ctx, "my-note", "content", nil)
Example (Basic)

Example_basic demonstrates how to initialize a Vault, save a note, and read it back.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/aretw0/loam"
	"github.com/aretw0/loam/pkg/core"
)

func main() {
	// Create a temporary directory for the example
	tmpDir, err := os.MkdirTemp("", "loam-example-*")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	// Initialize the Loam service (Vault) targeting the temporary directory.
	// WithAutoInit(true) ensures the underlying storage (git repo) is initialized.
	vault, err := loam.New(tmpDir, loam.WithAutoInit(true))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()

	// 1. Save a Document
	err = vault.SaveDocument(ctx, "hello-world", "This is my first note in Loam.", core.Metadata{
		"tags":   []string{"example"},
		"author": "Gopher",
	})
	if err != nil {
		log.Fatal(err)
	}

	// 2. Read it back
	doc, err := vault.GetDocument(ctx, "hello-world")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Found document: %s\n", doc.ID)
}
Output:

Found document: hello-world
Example (CsvNestedData)

Example_csvNestedData demonstrates Loam's "Smart CSV" capability, which automatically handles nested structures (like maps or slices) by serializing them as JSON within the CSV column.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/aretw0/loam"
)

func main() {
	// Setup: Temporary repository
	tmpDir, err := os.MkdirTemp("", "loam-csv-example-*")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	repo, err := loam.Init(filepath.Join(tmpDir, "vault"), loam.WithAutoInit(true))
	if err != nil {
		log.Fatal(err)
	}

	type Metrics struct {
		Host string            `json:"host"`
		Tags map[string]string `json:"tags"` // Nested Map
		Load []int             `json:"load"` // Nested Slice
	}

	metricsRepo := loam.NewTypedRepository[Metrics](repo)
	ctx := context.Background()

	// 1. Save complex data to CSV
	err = metricsRepo.Save(ctx, &loam.DocumentModel[Metrics]{
		ID: "metrics/server-01.csv", // .csv extension triggers CSV adapter
		Data: Metrics{
			Host: "server-01",
			Tags: map[string]string{"env": "prod", "region": "us-east"},
			Load: []int{10, 20, 15},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	// 2. Read it back
	// Loam automatically parses the JSON strings inside the CSV back into Maps and Slices.
	doc, err := metricsRepo.Get(ctx, "metrics/server-01.csv")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Host: %s\n", doc.Data.Host)
	fmt.Printf("Tag Region: %s\n", doc.Data.Tags["region"])
	fmt.Printf("Load: %v\n", doc.Data.Load)
}
Output:

Host: server-01
Tag Region: us-east
Load: [10 20 15]
Example (StrictMode)

Example_strictMode demonstrates how to enable global strict mode for type fidelity. This ensures that large integers (int64) are not lost as float64 during parsing across ALL supported formats (JSON, YAML, Markdown).

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/aretw0/loam"
)

func main() {
	// Setup
	tmpDir, err := os.MkdirTemp("", "loam-strict-*")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	// Initialize with Global Strict Mode
	// This applies strict parsing (json.Number) to all serializers.
	repo, err := loam.Init(filepath.Join(tmpDir, "vault"),
		loam.WithAutoInit(true),
		loam.WithStrict(true),
	)
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()

	// 1. JSON Example (Large Int)
	jsonContent := `{"big_id": 9223372036854775807, "type": "json"}`
	_ = os.WriteFile(filepath.Join(tmpDir, "vault", "strict.json"), []byte(jsonContent), 0644)

	// 2. YAML Example (Large Int)
	yamlContent := "big_id: 9223372036854775807\ntype: yaml"
	_ = os.WriteFile(filepath.Join(tmpDir, "vault", "strict.yaml"), []byte(yamlContent), 0644)

	// Read back and verify types
	for _, file := range []string{"strict.json", "strict.yaml"} {
		doc, err := repo.Get(ctx, file)
		if err != nil {
			log.Fatal(err)
		}

		val := doc.Metadata["big_id"]
		fmt.Printf("[%s] Type: %T\n", doc.Metadata["type"], val)
	}

}
Output:

[json] Type: json.Number
[yaml] Type: json.Number

Index

Examples

Constants

View Source
const (
	CommitTypeFeat     = platform.CommitTypeFeat
	CommitTypeFix      = platform.CommitTypeFix
	CommitTypeDocs     = platform.CommitTypeDocs
	CommitTypeStyle    = platform.CommitTypeStyle
	CommitTypeRefactor = platform.CommitTypeRefactor
	CommitTypePerf     = platform.CommitTypePerf
	CommitTypeTest     = platform.CommitTypeTest
	CommitTypeChore    = platform.CommitTypeChore
)

Variables

View Source
var Version string

Functions

func AppendFooter added in v0.5.1

func AppendFooter(msg string) string

AppendFooter appends the Loam footer to an arbitrary message.

func FindVaultRoot added in v0.8.1

func FindVaultRoot(startDir string) (string, error)

FindVaultRoot recursively looks upwards for a vault root indicator.

func FormatChangeReason added in v0.5.1

func FormatChangeReason(ctype, scope, subject, body string) string

FormatChangeReason builds a Conventional Commit message.

func Init added in v0.5.1

func Init(path string, opts ...Option) (core.Repository, error)

Init initializes a repository explicitly.

func IsDevRun added in v0.5.1

func IsDevRun() bool

IsDevRun checks if the current process is running via `go run` or `go test`.

func New added in v0.5.1

func New(path string, opts ...Option) (*core.Service, error)

New creates a new Loam Service.

func NewTypedRepository added in v0.8.1

func NewTypedRepository[T any](repo core.Repository) *typed.Repository[T]

NewTypedRepository creates a type-safe wrapper around an existing repository.

Example

ExampleNewTypedRepository demonstrates how to use the Generic Typed Wrapper for type safety.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/aretw0/loam"
)

func main() {
	// Setup: Temporary repository
	tmpDir, err := os.MkdirTemp("", "loam-typed-example-*")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	// Use loam.Init to get the Repository directly
	repo, err := loam.Init(filepath.Join(tmpDir, "vault"), loam.WithAutoInit(true))
	if err != nil {
		log.Fatal(err)
	}

	// Define your Domain Model
	type User struct {
		Name  string `json:"name"`
		Email string `json:"email"`
	}

	// Wrap the repository
	userRepo := loam.NewTypedRepository[User](repo)
	ctx := context.Background()

	// Save a typed document
	err = userRepo.Save(ctx, &loam.DocumentModel[User]{
		ID:      "users/alice",
		Content: "Alice's Profile",
		Data: User{
			Name:  "Alice",
			Email: "alice@example.com",
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	// Retrieve it back
	doc, err := userRepo.Get(ctx, "users/alice")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("User Name: %s\n", doc.Data.Name)
}
Output:

User Name: Alice

func NewTypedService added in v0.8.1

func NewTypedService[T any](svc *core.Service) *typed.Service[T]

NewTypedService creates a type-safe wrapper around an existing service.

func OpenTypedRepository added in v0.8.1

func OpenTypedRepository[T any](path string, opts ...Option) (*typed.Repository[T], error)

OpenTypedRepository simplifies creating a TypedRepository from a path.

func OpenTypedService added in v0.8.1

func OpenTypedService[T any](path string, opts ...Option) (*typed.Service[T], error)

OpenTypedService simplifies creating a TypedService from a path.

func ResolveVaultPath added in v0.5.1

func ResolveVaultPath(userPath string, forceTemp bool) string

ResolveVaultPath determines the actual path for the vault based on safety rules.

func Sync added in v0.5.1

func Sync(path string, opts ...Option) error

Sync performs a synchronization (pull/push) of the vault.

Types

type DocumentModel added in v0.6.0

type DocumentModel[T any] = typed.DocumentModel[T]

DocumentModel is a public alias for the typed document model.

type Option added in v0.5.1

type Option = platform.Option

Option defines a functional option for configuring Loam.

func WithAdapter added in v0.5.1

func WithAdapter(name string) Option

WithAdapter allows specifying the storage adapter to use by name.

func WithAutoInit added in v0.5.1

func WithAutoInit(auto bool) Option

WithAutoInit enables automatic initialization of the vault (creates directory and git init).

func WithContentExtraction added in v0.10.8

func WithContentExtraction(enabled bool) Option

WithContentExtraction controls whether JSON/YAML/CSV content fields are extracted into Document.Content. When disabled, the file payload is preserved 1:1 in Metadata.

func WithDevSafety added in v0.10.6

func WithDevSafety(enabled bool) Option

WithDevSafety controls the "Sandbox" safety mechanism (go run temp dir).

func WithEventBuffer added in v0.9.0

func WithEventBuffer(size int) Option

func WithForceTemp added in v0.5.1

func WithForceTemp(force bool) Option

WithForceTemp forces the use of a temporary directory (useful for testing).

func WithLogger added in v0.5.1

func WithLogger(logger *slog.Logger) Option

WithLogger sets the logger for the service.

func WithMarkdownBodyKey added in v0.10.8

func WithMarkdownBodyKey(key string) Option

WithMarkdownBodyKey sets the metadata key used to store Markdown body when content extraction is disabled. Defaults to "body".

func WithMustExist added in v0.5.1

func WithMustExist(must bool) Option

WithMustExist ensures the vault directory must already exist.

func WithReadOnly added in v0.10.6

func WithReadOnly(enabled bool) Option

WithReadOnly enables read-only mode (bypasses safety lock, prevents writes).

func WithRepository added in v0.5.1

func WithRepository(repo core.Repository) Option

WithRepository allows injecting a custom storage adapter.

func WithSerializer added in v0.10.2

func WithSerializer(ext string, s any) Option

WithSerializer registers a custom serializer for a specific extension. The serializer must implement the adapter's Serializer interface.

func WithStrict added in v0.10.4

func WithStrict(strict bool) Option

WithStrict enables strict mode for all default serializers. When enabled, numbers in JSON/YAML/Markdown will be parsed as json.Number (string based) to preserve precision of large integers.

func WithSystemDir added in v0.6.0

func WithSystemDir(name string) Option

WithSystemDir allows specifying the hidden directory name (e.g. ".loam").

func WithVersioning added in v0.5.1

func WithVersioning(enabled bool) Option

WithVersioning enables or disables version control (e.g. Git).

func WithWatcherErrorHandler added in v0.10.5

func WithWatcherErrorHandler(fn func(error)) Option

WithWatcherErrorHandler registers a callback to handle errors occurring during the Watch loop.

type TypedRepository added in v0.6.0

type TypedRepository[T any] = typed.Repository[T]

TypedRepository is a public alias for the typed repository.

type TypedService added in v0.8.1

type TypedService[T any] = typed.Service[T]

TypedService is a public alias for the typed service.

Directories

Path Synopsis
cmd
loam command
examples
demos/readonly command
internal
pkg
core
Document is the central entity of the domain.
Document is the central entity of the domain.
git

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL