Documentation
¶
Overview ¶
Package project is the generic projection engine behind pkg/skillsync, pkg/contexts, pkg/agentsync, and pkg/wiring: "project canonical content into per-client locations with lockfile-tracked ownership." The engine owns the unified lockfile (schema, two-tier versioning, migration from the legacy lockfiles, cross-process locking) and the shared vocabulary (states, dry-run actions, hash-scheme prefix, atomic writes, backup pruning).
A kind yields a set of (source, target) projection keys: the contexts adapter records one source ("global") fanned to N clients (plus one source per fragment in fragments mode); the skills adapter records one source per projected skill. Everything the kinds do differently on purpose stays in the kind packages: target tables, channel/strategy resolution, hashing (tree vs content), backup placement, status enumeration mode, rendering, and remediation text. The engine deliberately does not force a uniform Target or a shared sync loop; the frozen CLI contracts are per-kind and the characterization tests in cmd/gridctl arbitrate.
Index ¶
Constants ¶
const ( StateInSync = "in-sync" StateStale = "stale" StateDrifted = "drifted" StateTargetMissing = "target-missing" )
Projection states shared by every kind. Kinds may extend the vocabulary (contexts adds "unsupported" and "never-synced").
const ( ActionUpdated = "updated" ActionUnchanged = "unchanged" ActionError = "error" ActionSkippedDrift = "skipped-drift" ActionWouldUpdate = "would-update" )
Actions shared by every kind's sync results. Kind-specific actions (linked, copied, created, ...) stay in the kind packages.
const ChannelReasonModelPolicy = "model-policy"
ChannelReasonModelPolicy is the Entry.ChannelReason value marking a projection whose installed bytes carry a stack model preference rewrite. Defined once in the engine so the skill and agent kinds can never drift apart on the string.
const HashScheme = "sha256:"
HashScheme prefixes every stored hash so a future scheme change never presents as false drift (the pkg/pins lesson).
const LockVersion = 1
LockVersion is the breaking-change tier of the lockfile version: readers reject a newer version with ErrNewerLockVersion instead of silently clobbering state written by a newer gridctl.
const MaxBackups = 3
MaxBackups is the shared keep-newest retention for projection backups, whatever their per-kind placement policy.
Variables ¶
var ErrNewerLockVersion = errors.New("project lockfile was written by a newer gridctl version")
ErrNewerLockVersion signals a lockfile written by a newer gridctl. Callers must never paper over it: acting on state a newer version wrote risks silent data loss.
var ErrPathConflict = errors.New("destination path is already owned by another projection")
ErrPathConflict signals two projections claiming the same destination path. The unified lockfile exists to enforce the opposite invariant: one destination has exactly one owner.
Functions ¶
func AtomicWriteFile ¶
AtomicWriteFile writes data via a uniquely named temp file + rename in the target dir. Unique names keep concurrent writers from clobbering each other's in-flight temp file. Absorbed from the byte-identical copies in pkg/contexts and pkg/skillsync.
func StaleBackups ¶
StaleBackups returns the entries of a timestamped backup set beyond the newest keep, oldest first, for the caller to delete. Lexicographic order is chronological because backup names lead with a zero-padded "20060102-150405" timestamp. Placement and removal policy stay with the kind: contexts removes sibling files, skillsync removes out-of-tree directories, and both treat deletion as best-effort.
Types ¶
type Entry ¶
type Entry struct {
Kind Kind `yaml:"kind"`
Client string `yaml:"client"`
// Source names what is projected: the skill name for KindSkill, the
// scope ("global") for KindContext.
Source string `yaml:"source"`
// Path is the absolute destination path gridctl wrote or created.
Path string `yaml:"path"`
// KindSkill attributes.
Channel string `yaml:"channel,omitempty"`
CreatedByGridctl bool `yaml:"created_by_gridctl,omitempty"`
TreeHash string `yaml:"tree_hash,omitempty"`
// ChannelReason records why the CHANNEL diverges from what the user
// or target table chose: ChannelReasonModelPolicy marks a skill
// projection forced off symlink because its bytes carry a policy
// rewrite. Empty when the channel is the user's own choice (a --copy
// the policy merely rewrote keeps its empty reason, so the copy
// stays sticky when the policy goes away). Absent in pre-existing
// lockfiles, which migrate-on-read as empty.
ChannelReason string `yaml:"channel_reason,omitempty"`
// ModelValue records the model preference a policy rewrite wrote
// into the projected bytes; non-empty is the "bytes are rewritten"
// marker the preserve rule and adopt key on, and the value itself
// lets adopt distinguish the policy's write from a deliberate user
// edit of the same key. Empty for pass-through projections. Skill
// and agent kinds both use it.
ModelValue string `yaml:"model_value,omitempty"`
// KindContext attributes.
Strategy string `yaml:"strategy,omitempty"`
InstalledHash string `yaml:"installed_hash,omitempty"`
CanonicalHash string `yaml:"canonical_hash,omitempty"`
CreatedFile bool `yaml:"created_file,omitempty"`
// InputHashes records, for a compiled context target, each input
// fragment's canonical hash at sync time (fragment name -> hash), so
// staleness can name which fragment moved instead of only "the
// composite changed". Absent outside fragments mode.
InputHashes map[string]string `yaml:"input_hashes,omitempty"`
// KindWiring attributes. Path is the composite "<config path>#<entry
// name>" (one config file legitimately holds several owned entries, and
// the one-owner invariant keys on the full Path); ConfigPath is the real
// file path so no consumer parses the composite. Hashes is the short
// history of canonical value hashes gridctl wrote, newest last, so a
// shape change by a newer gridctl never reads as user drift.
ConfigPath string `yaml:"config_path,omitempty"`
Group string `yaml:"group,omitempty"`
ClientID string `yaml:"client_id,omitempty"`
Hashes []string `yaml:"hashes,omitempty"`
// Pack tags the projection with the pack that applied it (empty =
// not pack-managed). Any kind may carry it; `gridctl pack` uses it
// for scoped status and cascade removal.
Pack string `yaml:"pack,omitempty"`
SyncedAt time.Time `yaml:"synced_at"`
Extra map[string]any `yaml:",inline"`
}
Entry is one recorded (kind, source, client) projection. The primary key is (client, path): one destination path has exactly one owner. The kind-specific attribute fields form a union across the two kinds; the engine stores them but never interprets them, and each kind's adapter reads only its own. Extra preserves fields this binary does not understand, so a revision bump by a newer gridctl survives a rewrite by this one (Article XVII).
type Kind ¶
type Kind string
Kind identifies a projection tenant.
const ( // KindSkill projects registry skill directories into client skill // dirs (pkg/skillsync). KindSkill Kind = "skill" // KindContext projects the canonical global context file into client // context locations (pkg/contexts). KindContext Kind = "context" // KindAgent projects imported agent definitions into client agent // directories as single files (pkg/agentsync). KindAgent Kind = "agent" // KindWiring records ownership of gateway entries merged into client // MCP configs (pkg/wiring): key-level ownership inside files gridctl // does not otherwise own, per Article XVI. KindWiring Kind = "wiring" // KindContextFragment projects one context rule fragment as its own // file into a client rules directory (pkg/contexts fragments mode). // A separate kind from KindContext on purpose: contexts flushes its // per-client entries with ReplaceKind, and an older gridctl that only // knows KindContext must never be able to drop or clobber fragment // records it cannot represent. KindContextFragment Kind = "context-fragment" )
type Lock ¶
type Lock struct {
// contains filtered or unexported fields
}
Lock is an in-memory view of the lockfile, handed to Load and Mutate callers. Save is only valid inside Mutate.
func (*Lock) Entries ¶
Entries returns the entries of one kind in deterministic (source, client) order.
func (*Lock) Remove ¶
Remove deletes the record for (kind, client, source). Removal is an explicit engine-driven delete under the cross-process lock, never an inference from absence.
func (*Lock) ReplaceKind ¶
ReplaceKind makes entries the complete recorded set for one kind: each entry is Set, and records of that kind absent from entries are removed as explicit engine-driven deletes. Entries of other kinds and unknown file-level fields ride along untouched. This is how a kind view flushes back into the lock.
func (*Lock) Save ¶
Save persists the lock atomically. Kind managers call it after every recorded mutation (skillsync's persistIfRecorded crash-safety property: a crash mid-pass must never leave artifacts on disk the lockfile does not own).
func (*Lock) Set ¶
Set records an entry, replacing any previous record for the same (kind, client, source) and enforcing the one-owner invariant: a path already owned by a different projection is refused, never stolen. When e carries no Extra, the previous record's unknown fields are carried forward, so re-recording an entry by an older binary never strips fields a newer revision wrote (Article XVII); pass an empty non-nil map to deliberately clear them.
type LockFile ¶
type LockFile struct {
Version int `yaml:"version"`
Revision int `yaml:"revision"`
Projections []*Entry `yaml:"projections"`
Extra map[string]any `yaml:",inline"`
}
LockFile is the on-disk shape of the unified projection lockfile at ~/.gridctl/project.lock.yaml. Absence of an entry means "unknown, do not touch," never "remove": removal of a projection happens only as an explicit engine-driven delete under the cross-process lock (Article XVI).
func ReadLockFile ¶
ReadLockFile loads the unified lockfile from path. A missing file is the normal nothing-projected state and yields an empty lock.
type LockfileState ¶
type LockfileState struct {
// UnifiedPath is where the unified lockfile lives (or will live).
UnifiedPath string
// Unified reports whether the unified lockfile exists.
Unified bool
// LegacySkill and LegacyContext report live (version 1) legacy
// lockfiles awaiting migration.
LegacySkill bool
LegacyContext bool
// Tombstones lists legacy paths already tombstoned by a migration.
Tombstones []string
// BackupRoot is where migration backups are kept.
BackupRoot string
}
LockfileState describes which projection lockfiles exist on disk, for surfaces like `gridctl doctor` that report migration state without loading entries.
func InspectLockfiles ¶
func InspectLockfiles(home string) LockfileState
InspectLockfiles reports the on-disk projection lockfile state under home. Read-only; unreadable or malformed legacy files are reported as live so the caller surfaces them rather than ignoring them.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store owns the unified projection lockfile under <home>/.gridctl and the cross-process lock guarding it. All paths resolve against home, so tests point it at a temp dir. Two Store instances over the same home (or two processes) serialize on the flock; in-process higher-level serialization stays with the kind managers' mutexes.
func (*Store) Load ¶
Load returns a read-only view of the projection state without taking the cross-process lock (the lockfile is written atomically, so lock-free reads see a consistent file). Before migration, the two legacy lockfiles are merged in memory; nothing is written.
func (*Store) Mutate ¶
Mutate runs fn with a writable view while holding the cross-process lock. On the first mutating operation after an upgrade the legacy lockfiles migrate to the unified file (backups, then tombstones). Dry-run passes take no lock at all: they read the same merged view (the lockfile is written atomically, so lock-free reads are consistent), skip the on-disk migration, refuse Save, and therefore can neither write anything nor fail on lock contention.