store

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

@CODE:ARCHIVE-001 @CODE:ARCHIVE-002 @CODE:ARCHIVE-003 @CODE:ARCHIVE-004 @CODE:ARCHIVE-007 @CODE:ARCHIVE-008

@CODE:WS-021

@CODE:STORE-001 @CODE:STORE-002

@CODE:STORE-003 @CODE:STORE-004 @CODE:STORE-007 @CODE:STORE-009

@CODE:LOCK-001

@CODE:WS-004 @CODE:WS-005 @CODE:WS-006

@CODE:STORE-005 @CODE:STORE-006 @CODE:STORE-008 @CODE:STORE-010 @CODE:LOCK-002 @CODE:LOCK-003

Index

Constants

View Source
const (
	DataFileName    = "todos.jsonl"
	ArchiveFileName = "archive.jsonl"
)

DataFileName is the primary JSONL data file; ArchiveFileName is reserved for the archive move owned by Feature 007.

View Source
const (
	RegistryFileName      = "workspaces.jsonl"
	RegistryFormatVersion = 1
)

RegistryFileName is the workspace registry's own shared file under the data directory (FR-006). RegistryFormatVersion starts at 1 (PRD §4.15 / §IX): additive changes keep the version; unknown fields are preserved verbatim.

View Source
const DefaultRetentionDays = 30

DefaultRetentionDays is the archive retention default when RetentionEnv is unset or empty (settled — PRD Open Items, 2026-07-19).

View Source
const FormatVersion = 1

FormatVersion is the current data-file format version (PRD §4.15). Feature 001 exercises version 1 only; the incompatible-bump path is reserved forward policy.

View Source
const RetentionEnv = "CORK_ARCHIVE_RETENTION_DAYS"

RetentionEnv is the environment variable that overrides the completed-item archive retention period, expressed as a whole number of days (Feature 007, clarified 2026-07-21). It follows the same env-first configuration pattern as CORK_DATA_DIR — cork carries no config-file mechanism.

Variables

This section is empty.

Functions

func ArchiveFilePath

func ArchiveFilePath(dir string) string

ArchiveFilePath returns the reserved archive.jsonl path within dir (FR-002).

func DataFilePath

func DataFilePath(dir string) string

DataFilePath returns the todos.jsonl path within dir (FR-002).

func IDSet

func IDSet(items []Item) map[string]struct{}

IDSet returns the set of IDs present in items, for passing to NewID.

func NewID

func NewID(existing map[string]struct{}) (string, error)

NewID returns an ID guaranteed unique against existing (FR-004 / SC-004). Raw 40-bit entropy makes a collision rare, but "rare" is not the zero-collision contract, so the candidate is regenerated on any collision until it is unused. It returns an error rather than panicking when the entropy source fails or the id space cannot yield a fresh id within maxIDAttempts.

func ResolveDataDir

func ResolveDataDir() (string, error)

ResolveDataDir resolves the data directory by precedence (FR-001): CORK_DATA_DIR -> $XDG_DATA_HOME/cork -> ~/.local/share/cork. It resolves the path only; it neither creates nor verifies the directory (loud-fail on access is Open's job, FR-008).

func ResolveRetention

func ResolveRetention() (time.Duration, error)

ResolveRetention resolves the archive retention period from RetentionEnv, interpreting its value as a whole number of days, and falls back to DefaultRetentionDays when the variable is unset or empty (FR-003). A value that is not a positive whole number of days is a loud failure — an explicit error, never a silent fallback to the default (FR-003a), matching cork's loud-fail-on-bad-configuration posture.

Types

type Item

type Item struct {
	ID          string
	Text        string
	Workspace   string
	Pinned      bool
	Done        bool
	CreatedBy   string
	CreatedAt   time.Time
	CompletedAt *time.Time
	Version     int
	// contains filtered or unexported fields
}

Item is one stored todo record. The full field set is laid down here (stub-and-forward); several fields are activated by later Features. Fields the running binary does not recognize are preserved verbatim in extra across a read-modify-write cycle (FR-007).

func (Item) MarshalJSON

func (it Item) MarshalJSON() ([]byte, error)

MarshalJSON emits the typed fields plus every preserved extra field. Known keys always win over a stale extra copy of the same key.

func (*Item) UnmarshalJSON

func (it *Item) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a record, mapping known keys to typed fields and keeping every other key in extra untouched (FR-007). A record without a version key defaults to FormatVersion (FR-009).

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is the cork-owned workspace registry bound to a resolved, accessible data directory. It reads no external workflow's registry (FR-005). Its file is shared by every environment that mounts the same data directory (FR-006).

func OpenRegistry

func OpenRegistry(dir string) (*Registry, error)

OpenRegistry binds a Registry to dir, loud-failing (explicit error, creates nothing) when the directory is missing or inaccessible — the same contract as store.Open. It never creates the registry file; the first Add creates it.

func (*Registry) Add

func (r *Registry) Add(name string) error

Add registers name (FR-001). It is idempotent: adding a name already present leaves the registry unchanged (one entry, original position) and is not an error (FR-008).

func (*Registry) List

func (r *Registry) List() ([]string, error)

List returns the registered workspace names in registration order (FR-002).

func (*Registry) Remove

func (r *Registry) Remove(name string) error

Remove unregisters name (FR-003). Removing a name that is not registered is a not-found error and writes nothing (FR-009). It never touches items filed under that workspace — those live in todos.jsonl and are left intact (FR-004).

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is bound to a resolved, accessible data directory.

func Open

func Open(dir string) (*Store, error)

Open binds a Store to dir. It loud-fails (explicit error, creates nothing) when the directory is missing or inaccessible (FR-008). It never creates an empty data file — a broken mount must never masquerade as an empty list.

func (*Store) Read

func (s *Store) Read() ([]Item, error)

Read returns the records in the data file. A genuinely absent data file (the data directory is still present) reads as zero records without creating anything (first-run state, FR-010); an existing empty file is likewise zero records. But an ENOENT caused by the data directory itself having gone away (unmount, removal) is a loud failure, never an empty list — a broken mount must never masquerade as "no todos" (FR-008/SC-003). A malformed line surfaces a parse error rather than being silently skipped (FR-005).

func (*Store) ReadArchive

func (s *Store) ReadArchive() ([]Item, error)

ReadArchive returns the archived records. It is a plain read (no lock), used by the archived-inclusion view (FR-004).

func (*Store) SweepArchive

func (s *Store) SweepArchive(retention time.Duration, now time.Time) (moved int, err error)

SweepArchive moves every completed item whose completion age exceeds retention out of todos.jsonl and into archive.jsonl, returning how many were moved (FR-001/FR-002). It is opportunistic and write-free when nothing is eligible.

Eligibility: Done && CompletedAt != nil && now-CompletedAt > retention (strictly greater — an item exactly at the boundary is kept, plan §2.2/§2.3).

Safety (FR-006 / plan §3 transaction boundary): the move holds the todo lock (outer) and the archive lock (inner) in that fixed order — never the reverse, so it cannot deadlock a concurrent rm that takes the two independently — and writes the archive FIRST, then rewrites todos without the moved items. A crash between the two writes therefore leaves an item in both files (a recoverable duplicate), never in neither (no data loss). The archive append dedups by id so re-sweeping a still-present main-file copy never creates a second archive line (INV-3). Unknown fields ride along untouched because whole Item values are moved (FR-007/ARCHIVE-008).

func (*Store) Update

func (s *Store) Update(fn func(items []Item) ([]Item, error)) error

Update runs a mutating read-modify-write under an exclusive file lock: it acquires the todo lock, reads the current items, passes them to fn, writes fn's returned slice via the atomic temp-file+rename writer, then releases the lock. Because the whole cycle is serialized, concurrent writers never clobber each other's items (different items are all preserved); item-level last-write-wins falls out because each transaction reads the latest file and only the later lock holder's version of a contended item survives (Feature 006 concurrency contract).

fn must not retain the slice past its return, and must NOT call Update (or any other locked mutation) on the same data directory from within itself: the file lock is not reentrant, so a nested acquisition on the same lock file would block forever. All current call sites pass a self-contained closure.

func (*Store) UpdateArchive

func (s *Store) UpdateArchive(fn func(items []Item) ([]Item, error)) error

UpdateArchive runs a mutating read-modify-write on archive.jsonl under the archive's own exclusive file lock, mirroring Store.Update. It lets a caller (the CLI's archive-aware rm) resolve and remove a record from the archive without re-implementing the lock/atomic-rename contract. fn must not call another locked archive mutation on the same directory (the lock is not reentrant).

Jump to

Keyboard shortcuts

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