cairn

package
v1.2.8 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package cairn is the store behind nova-cairn: session records holding a caller's exact words, each with a clock stamp, a stable identifier and a source pointer, and a bounded index and coverage count read back from them.

It does nothing else. There is no seal, no consume, no delete, no grading, no consolidation and no liveness inference: those are separate choices a caller never gets by accident. The store is plain files under a directory the caller names, and a write is synced to disk before success is reported, with no Redis and no remote: local persistence is reported apart from remote publication, and neither implies the other.

A store holds a session in one of two shapes, and both are read as they stand; a store is never converted to suit the tool.

The nested shape is the one open creates:

sessions/<session>.md        the readable record; its header is convention only
entries/<session>/<id>.json  one file per entry, the source of truth
log.jsonl                    append-only event log: the open records and the appends

The flat shape is one markdown file per session directly under the store, <store>/<session>.md, kept by hand or by another tool. The file is the record: an append lands a dated `## <stamp> — <entry>` section at its end, and no entries/ directory, log or index appears beside it. The nested record wins when a store holds both for one session.

Each entry file is written atomically through pkg/atomicfile (an exclusive temporary file beside the target, an explicit mode, fsync, then rename), so a retry after an interrupted append finishes the pointer line without duplicating the entry and without touching other writers' files. A stale temporary file is never indexed.

Index

Constants

View Source
const (
	PublishNever     = "never"
	PublishManual    = "manual"
	PublishDeferred  = "deferred"
	PublishImmediate = "immediate"
	// PublishUnknown is what a flat record reports: its format stores no policy.
	PublishUnknown = "unknown"
)

Publish policies name who publishes a checkpoint and when. This package implements no transport: the tool prints published=false for every append and receipt, and the policy travels with the entry, so a later explicit act can carry it.

View Source
const IDRule = "an id names exactly one file or directory of that name inside the store, on every platform: " +
	"nonempty, at most 128 bytes, no whitespace or control characters, none of / \\ : * ? \" < > |, " +
	"not only dots, no `..`, no trailing dot, and not a Windows device name (CON, PRN, AUX, NUL, COM1-9, LPT1-9, with or without an extension)"

IDRule is what ValidID asks of a session or entry id, for a refusal to quote.

Variables

Policies is the publication policies a caller may choose, in the order help names them.

Functions

func EntryText

func EntryText(store, session, id string) (string, error)

EntryText returns the stored words of a nested entry byte for byte. A flat record returns the section body in the form its reader indexes.

func Open

func Open(store, session, source string, now time.Time, publish string) error

Open starts one session record, or re-opens it: a re-open naming the recorded policy (and the recorded source, when it names one) changes nothing, and one naming another is a conflict. Concurrent records coexist: opening a second session never touches the first.

func ValidID

func ValidID(s string) bool

ValidID keeps an id one token on every output line and exactly one file or directory of that name inside the store: an id becomes a path component (sessions/<id>.md, <store>/<id>.md, entries/<id>/, entries/<s>/<id>.json), so every id that a path would read as something else is refused. "." would make entries/./ the entries directory itself, so an append lands where no index looks; ".." and any id holding it climb out; an id of only dots is one of those; a trailing dot or blank is stripped by Windows, folding "s." into "s"; a separator splits the id into two components; a device name or a Windows-forbidden character names no file at all there.

func ValidPublish

func ValidPublish(p string) bool

ValidPublish reports whether p is one of Policies.

Types

type AppendResult

type AppendResult struct {
	Stamp     time.Time // the stamp actually stored, including on a duplicate retry
	Persisted bool
	Policy    string
	// Source is the pointer the entry carries: its own --source, or the
	// session's when the append named none.
	Source    string
	Duplicate bool
}

AppendResult separates what this package guarantees from what it does not: Persisted is true once the words are fsync-durable on this machine (false only for a plan, PlanAppend, of words not yet stored); the tool prints the remote fact, published=false, apart from it.

func Append

func Append(store, session, id, text, source string, now time.Time, publish string) (AppendResult, error)

Append stores the caller's exact words under a stable entry id with a clock stamp and a source pointer. A retry of the same request succeeds with Duplicate=true and no second entry; the same id with other words is a conflict, never an overwrite. Offline use succeeds: the result carries Persisted=true and the tool prints published=false, because local durability never waits for a remote.

An empty source carries the session's, and an empty publish the session's recorded policy: open named where the record points back to and who publishes it, and an append that names neither came from the same place under the same policy. Both are read before any write, so a log that cannot be read refuses with nothing written.

func PlanAppend

func PlanAppend(store, session, id, text, source string, now time.Time, publish string) (AppendResult, error)

PlanAppend is Append with nothing written: every check Append makes, the same error, and the result the write would report, with Persisted false unless the words are already stored (a duplicate).

type ConflictError

type ConflictError struct{ Msg, Remedy string }

ConflictError is a write that must not silently win: the same entry id carrying different words, or a re-open naming another policy or source. It is the caller's failure (exit 1), not a crash. Remedy is the command to run next.

func (*ConflictError) Error

func (e *ConflictError) Error() string

type IndexRow

type IndexRow struct {
	Session string
	ID      string
	Stamp   time.Time
	Source  string
	Bytes   int
}

IndexRow is one row of the entry index.

func Index

func Index(store, session string) ([]IndexRow, []string, int, error)

Index builds the entry index from the stored entries: no words are recopied, entries are linked by session and entry id, and a stale *.tmp from an interrupted append is never a row. The sessions the store holds, from sessions/<id>.md and a flat <store>/<id>.md, come back from the same walk, so index prints and counts every session, an empty one included, without reading the store a second time. session "" indexes every record; a named session is the only one returned. It returns the rows, the session ids and the total.

type Ledger

type Ledger struct {
	Sessions int
	Entries  int
}

Ledger is the coverage count, derived from the store rather than remembered: how many session records exist and how many entries they hold.

type NotFoundError

type NotFoundError struct{ Msg, Remedy string }

NotFoundError is a use before open, or a read of an entry nothing stored. Remedy is the command to run next.

func (*NotFoundError) Error

func (e *NotFoundError) Error() string

type OpenRecord

type OpenRecord struct {
	Source, Publish string
	Opened          time.Time
	Found           bool
}

OpenRecord is what a session's open recorded in log.jsonl: its source, its publication policy and the opened stamp. Found is false when the log holds no open record for the session (a flat store, or no log at all). Opened is zero when the record names no stamp. Index prints the policy and the stamp on the session line (docs/SPEC-CAIRN.md, the index verb).

func PlanOpen

func PlanOpen(store, session, source string, now time.Time, publish string) (OpenRecord, error)

PlanOpen is Open with nothing written: every check Open makes and the same error, so a dry run refuses what the real run would. It returns the open record that would stand after the open (Found false for a flat record, which records none).

func ReadOpen

func ReadOpen(store, session string) (OpenRecord, error)

ReadOpen reads the session's open record from log.jsonl. The session file's header is convention only and is never parsed, so the log is where the pointer and the policy are read from.

Corrupt provenance never reads as none. An absent log answers Found=false with no error, as does a log with no open record for the session. A log that exists and cannot be read is an error, and so is a line that may be this session's open record and does not decode as one: a line naming the session and the open event that is not valid JSON, or whose fields are not strings. Answering "none" for either would let an append file source=- over a pointer open recorded.

type ReceiptInfo

type ReceiptInfo struct {
	Session   string
	ID        string
	Stamp     time.Time
	Source    string
	Bytes     int
	Policy    string
	Persisted bool
}

ReceiptInfo is what receipt and index read back: the stamp, the pointer, the size, and the persisted fact the append reported; the tool prints published=false beside it.

func Receipt

func Receipt(store, session, id string) (ReceiptInfo, error)

Receipt names what was preserved for one entry: its stamp, pointers and size, with the persisted fact repeated and published=false printed beside it, so a reader never has to infer the remote from the local.

Jump to

Keyboard shortcuts

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