Help us shape the future of Go! Take the 2026 Go Developer Survey

ledger

package
v1.4.13 Latest Latest
Warning

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

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

Documentation

Overview

Bootstrap: derive a first ledger from what is on disk.

Purpose: a project that installed bundles before the ledger existed has no record of why its plugins are present. Bootstrap rebuilds that record from the installed plugin directories plus bundle membership (bundles.json). Inputs: Sources, built by the caller from internal/bundle and internal/plugin. Outputs: a valid Ledger. Constraints: a bundle counts as installed only when it is installable as a unit and EVERY member plugin is installed (a bundle install is all or nothing, with rollback). A plugin referenced by no installed bundle is explicit. Overlap is honoured: installed_by lists every installed bundle that contains the plugin. A plugin installed on its own AND later covered by a bundle cannot be told apart from the bundle install on disk; bootstrap treats it as bundle-owned, and P7-PLUG-19/20 record explicit installs from then on.

Package ledger is the bundle install-state ledger (contract:cli.bundle-ledger v1, P7 PLUG D6): per plugin directory, which bundles are installed and, per plugin, installed_by / explicit / tier / version / checksum.

Purpose: record WHY a plugin is present, so refcounted bundle remove (P7-PLUG-20), app-ready (P7-PLUG-19) and the surfaces (P7-SURF-13) stop inferring it from plugin directories. Inputs: a plugin directory (the file lives at <pluginDir>/.nself-state/bundles.json) and, for bootstrap, Sources (bundle membership plus installed plugins). Outputs: a validated Ledger, written atomically (temp + rename) under a file lock. The file is machine-owned: its first key is _generated and nothing in it is meant to be edited by hand. Constraints: this package imports neither internal/bundle nor internal/plugin, so bundle install/remove can import it without a cycle; callers adapt their own types to Sources. No new dependency.

Mutations: the three edits P7-PLUG-19 (app-ready) and P7-PLUG-20 (refcounted remove) make to a Ledger.

Purpose: keep the installed_by / explicit rules in one place (PLUG D6): a bundle remove drops a plugin only when installed_by becomes empty AND explicit is false. Inputs: a *Ledger (from Store.Update) and the bundle or plugin to record. Outputs: the Ledger is changed in place; RemoveBundle returns the plugins the caller may now remove. Constraints: pure, no I/O, and nothing here changes install or remove behaviour; callers decide what to do with RemoveBundle's answer.

Parse: read and strictly validate ledger bytes.

Purpose: turn a ledger file into a Ledger, refusing anything the schema or the contract refuses: a first key other than _generated, duplicate keys at any depth, unknown keys, missing or null required keys, trailing data, oversize input, and every Validate failure. A damaged ledger is reported, never replaced by a plausible default. Inputs: the file bytes. Outputs: a Ledger or an error. Constraints: pure; no file access (Store reads and caps the file).

Store: read and atomically update the ledger file of one plugin directory.

Purpose: the only code that touches <pluginDir>/.nself-state/bundles.json. Inputs: the plugin directory ($NSELF_PLUGIN_DIR or ~/.nself/plugins) and an optional Sources func used to bootstrap. The ledger lives with the plugins it describes (D-0284 option A), so it is the same for every project that shares that plugin directory and needs no project root. Outputs: Load (read only), Update (read, change, validate, write). Constraints: writers take an OS lock on bundles.json.lock (next to the ledger), write a temp file in the same directory, fsync it, then rename over the ledger, so a reader sees the old or the new file, never a partial one, and a crash between the temp write and the rename leaves the old ledger intact. The file is 0644 (it holds no secret), its directory 0700 (same as the other state directories). The directory name starts with a dot and holds no plugin.json, so plugin scans skip it. Load never writes and never creates a directory.

View: the per-bundle slice of the ledger that `nself bundle list --json` prints under the additive `ledger` key of each row.

Purpose: show a bundle's recorded state without exposing the whole file. Inputs: a Ledger, a bundle slug and that bundle's member plugin slugs. Outputs: a BundleView whose plugin list is sorted and never null. Constraints: pure; no file access.

Index

Constants

View Source
const (
	// Generated is the value of the first key, _generated.
	Generated = "GENERATED BY nself — DO NOT HAND EDIT"
	// SchemaVersion is the only schema_version this package reads or writes.
	SchemaVersion = 1

	// TierFree and TierLicensed are the two tiers a plugin record may carry.
	TierFree     = "free"
	TierLicensed = "licensed"

	// MaxBytes caps the size of a ledger file that Parse and Load accept.
	MaxBytes = 4 << 20

	// SlugPattern is the shape of a bundle or plugin slug used as a map key.
	SlugPattern = `^[a-z0-9][a-z0-9._-]*$`
	// ChecksumPattern is a lowercase hex sha256, or empty when unknown.
	ChecksumPattern = `^([0-9a-f]{64})?$`
)
View Source
const StateDirName = ".nself-state"

StateDirName is the directory inside the plugin directory that holds the ledger.

Variables

Tiers lists the accepted tier values.

Functions

This section is empty.

Types

type BundleDef

type BundleDef struct {
	Slug string
	// Installable is false for structural bundles (task, nself-plus) that are
	// never installed as a unit; they are never recorded as installed.
	Installable bool
	Plugins     []string
}

BundleDef is one bundle's membership.

type BundleRecord

type BundleRecord struct {
	InstalledAt string `json:"installed_at"` // RFC 3339 UTC
}

BundleRecord is one installed bundle.

type BundleView

type BundleView struct {
	Installed   bool         `json:"installed"`
	InstalledAt string       `json:"installed_at,omitempty"`
	Plugins     []PluginView `json:"plugins"`
}

BundleView is one bundle's ledger state.

type InstalledPlugin

type InstalledPlugin struct {
	Name     string
	Version  string
	Tier     string // free | licensed; anything else is treated as free
	Checksum string // lowercase hex sha256 or empty
}

InstalledPlugin is one plugin found on disk.

type Ledger

type Ledger struct {
	Generated     string                  `json:"_generated"`
	SchemaVersion int                     `json:"schema_version"`
	Bundles       map[string]BundleRecord `json:"bundles"`
	Plugins       map[string]PluginRecord `json:"plugins"`
}

Ledger is the file. Field order is the on-disk key order: _generated first.

func Bootstrap

func Bootstrap(s Sources) Ledger

Bootstrap builds the first ledger from s, dropping what cannot be recorded.

func BootstrapReport

func BootstrapReport(s Sources) (Ledger, []string)

BootstrapReport builds the first ledger from s. A plugin or bundle whose name is not a valid slug cannot be written to the ledger (one such name would make every later write fail), so it is skipped and returned as a description the caller can show. A bundle with a skipped member is not installed.

func New

func New() Ledger

New returns an empty, valid ledger.

func Parse

func Parse(data []byte) (Ledger, error)

Parse reads and validates ledger bytes. Unknown keys, a first key other than _generated, trailing data and every Validate failure are errors: a damaged ledger is reported, never replaced by a plausible default.

func (*Ledger) AddBundle

func (l *Ledger) AddBundle(slug string, members []InstalledPlugin, at time.Time)

AddBundle records bundle slug as installed at `at` with the given member plugins. Existing plugin records keep their explicit flag; the bundle is added to their installed_by. A new plugin record is explicit=false. Members may be passed by name only: a fact the input does not carry (version, checksum, a tier other than free or licensed) keeps its recorded value. Re-adding a bundle that is already recorded keeps its first installed_at.

func (Ledger) Clone

func (l Ledger) Clone() Ledger

Clone returns a deep copy.

func (*Ledger) MarkExplicit

func (l *Ledger) MarkExplicit(p InstalledPlugin)

MarkExplicit records that the owner installed the plugin on its own, so a bundle remove keeps it. A plugin not yet recorded is added with an empty installed_by.

func (Ledger) Marshal

func (l Ledger) Marshal() ([]byte, error)

Marshal validates and renders the canonical bytes: 2-space indent, sorted map keys, _generated first, trailing newline. It does not change l.

func (*Ledger) RemoveBundle

func (l *Ledger) RemoveBundle(slug string) ([]string, error)

RemoveBundle forgets bundle slug and returns, sorted, the plugins that no installed bundle references any more and that were not installed explicitly: those are the ones a bundle remove may delete. A plugin still referenced by another bundle, or explicit, stays in the ledger and is not returned. An unknown bundle is an error and changes nothing.

func (Ledger) Validate

func (l Ledger) Validate() error

Validate checks every rule the JSON Schema cannot express alone: references between records and the keep-or-remove invariant.

func (Ledger) View

func (l Ledger) View(slug string, members []string) BundleView

View returns the state of bundle slug and of those of its members the ledger records.

type PluginRecord

type PluginRecord struct {
	// InstalledBy lists the installed bundles that reference the plugin, sorted.
	InstalledBy []string `json:"installed_by"`
	// Explicit is true when the owner installed the plugin on its own, so a
	// bundle remove must keep it.
	Explicit bool   `json:"explicit"`
	Tier     string `json:"tier"`
	Version  string `json:"version"`
	// Checksum is the lowercase hex sha256 of the installed artifact, or empty
	// when the install did not record one.
	Checksum string `json:"checksum"`
}

PluginRecord says why one plugin is present.

type PluginView

type PluginView struct {
	Slug        string   `json:"slug"`
	InstalledBy []string `json:"installed_by"`
	Explicit    bool     `json:"explicit"`
	Tier        string   `json:"tier"`
	Version     string   `json:"version"`
	Checksum    string   `json:"checksum"`
}

PluginView is one member plugin as the ledger records it.

type Sources

type Sources struct {
	Bundles []BundleDef
	Plugins []InstalledPlugin
	// Now stamps installed_at; nil means time.Now.
	Now func() time.Time
}

Sources is everything Bootstrap reads. A Sources value must come from a successful read of the plugin directory and bundle membership: an unreadable source is an error for the caller (see Store.Sources), never an empty Sources.

type Store

type Store struct {

	// Sources supplies bootstrap input when the file does not exist yet. It
	// returns an error when the installed plugins or bundle membership cannot be
	// read. A missing file with a nil Sources, or a Sources error, is an error
	// (never an empty ledger): an empty ledger would be written and then hide
	// the real install state, because bootstrap runs only once.
	Sources func() (Sources, error)
	// Warn, when set, receives what Bootstrap had to skip (invalid slugs).
	Warn func(msg string)
	// contains filtered or unexported fields
}

Store is the ledger of one plugin directory.

func NewStore

func NewStore(pluginDir string) *Store

NewStore returns the store for the plugin directory pluginDir.

func (*Store) Dir

func (s *Store) Dir() string

Dir is <pluginDir>/.nself-state.

func (*Store) EnsureBootstrapped

func (s *Store) EnsureBootstrapped() (created bool, err error)

EnsureBootstrapped writes the bootstrap ledger when no file exists and reports whether it did. An existing ledger is left untouched.

func (*Store) Load

func (s *Store) Load() (l Ledger, persisted bool, err error)

Load returns the ledger and whether it came from the file. A missing file is not an error: the bootstrap result is returned (persisted=false) and nothing is written. A damaged file is an error.

func (*Store) Path

func (s *Store) Path() string

Path is the ledger file.

func (*Store) Update

func (s *Store) Update(fn func(*Ledger) error) error

Update runs fn on the current ledger (bootstrapped first when the file is missing) under the writer lock and writes the result. fn's error aborts the write. Two concurrent Updates never lose each other's change.

Jump to

Keyboard shortcuts

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