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
- func ArchiveFilePath(dir string) string
- func DataFilePath(dir string) string
- func IDSet(items []Item) map[string]struct{}
- func NewID(existing map[string]struct{}) (string, error)
- func ResolveDataDir() (string, error)
- func ResolveRetention() (time.Duration, error)
- type Item
- type Registry
- type Store
- func (s *Store) Read() ([]Item, error)
- func (s *Store) ReadArchive() ([]Item, error)
- func (s *Store) SweepArchive(retention time.Duration, now time.Time) (moved int, err error)
- func (s *Store) Update(fn func(items []Item) ([]Item, error)) error
- func (s *Store) UpdateArchive(fn func(items []Item) ([]Item, error)) error
Constants ¶
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.
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.
const DefaultRetentionDays = 30
DefaultRetentionDays is the archive retention default when RetentionEnv is unset or empty (settled — PRD Open Items, 2026-07-19).
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.
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 ¶
ArchiveFilePath returns the reserved archive.jsonl path within dir (FR-002).
func DataFilePath ¶
DataFilePath returns the todos.jsonl path within dir (FR-002).
func NewID ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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).
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is bound to a resolved, accessible data directory.
func Open ¶
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 ¶
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 ¶
ReadArchive returns the archived records. It is a plain read (no lock), used by the archived-inclusion view (FR-004).
func (*Store) SweepArchive ¶
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 ¶
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 ¶
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).