Documentation
¶
Overview ¶
Package core reconciles a tree of declarative files against a remote API: it parses them, resolves the references between them, orders the work, diffs it against what the server holds, and records the outcome in a lockfile.
It knows nothing about any particular API beyond its general shape: JSON bodies, tagged string IDs, optional versions. Everything domain-specific — what kinds exist, which fields the server owns, what the files and lockfile are called, how to talk to it — arrives through a Registry and a Client. The sibling claude package is the implementation for the Claude Developer Platform.
A run is Loader → Planner → Plan → Applier, with the Lockfile carrying state between runs. The files, in that order:
spec.go KindSpec and Field: what a domain says about each kind registry.go Registry and Schema: the set of kinds, and the file layout client.go Client, Request and Payload: the API a domain provides source.go Source and Candidate: one file read and decoded loader.go Loader: paths to a closed set of Sources resolve.go following the references a Source makes refs.go reference syntax and Target, what a reference resolves to include.go splicing data files into list fields at load time fetch.go URLFetcher and the GitHub implementation graph.go dependency order and cycle reporting desired.go rendering the request body with references filled in plan.go Planner and Plan: what apply would do, and why diff.go field-level Diff, drift comparison, redaction version.go versions from the server, into the lockfile, and back out apply.go Applier: executing a Plan, saving state after each write lock.go Lockfile: the committed state file hash.go canonical JSON and fingerprints bodypath.go dotted-path helpers over decoded JSON
Index ¶
- Constants
- Variables
- func EncodeID(t Target) any
- func EncodeObject(typeName, idField string) func(Target) any
- func Hint(err error) string
- func IsEmptyValue(v any) bool
- func ParseYAMLMap(data []byte, what string) (map[string]any, error)
- func WithHint(err error, hint string) error
- type Action
- type Applied
- type Applier
- type Candidate
- type Change
- type Client
- type Destroy
- type Diff
- type DiffKind
- type FetchedURL
- type Field
- type Fields
- type Found
- type GitHubFetcher
- type ItemDiff
- type Kind
- type KindSpec
- type Loader
- func (l *Loader) Add(ctx context.Context, paths []string) error
- func (l *Loader) AddKeys(ctx context.Context, keys []string) error
- func (l *Loader) Discover(dir string) ([]Found, error)
- func (l *Loader) Pin(pins map[string]URLPin)
- func (l *Loader) Sources() map[string]*Source
- func (l *Loader) TopoOrder() ([]string, error)
- type LockEntry
- type Lockfile
- type Origin
- type Payload
- type Plan
- type Planner
- type Ref
- type RefSlot
- type Registry
- type Request
- type Result
- type Schema
- type Source
- type Target
- type URLFetcher
- type URLPin
Constants ¶
const KnownAfterApply = "(known after apply)"
KnownAfterApply is the placeholder a plan shows where a value will only exist once the resource it depends on has been created.
Variables ¶
var ErrNotFound = errors.New("resource not found")
ErrNotFound is what a Client returns when a recorded resource is gone from the server — deleted out of band, or belonging to a different account than the credentials in use.
Functions ¶
func EncodeObject ¶
EncodeObject renders a reference as a discriminated object: a `type`, the ID under idField, and the pinned version when the target has one. This is the shape most APIs use for a typed reference; a domain names the discriminator and the field.
func IsEmptyValue ¶
IsEmptyValue reports whether v holds nothing: nil, the empty string, or an empty list or map. Diffing and rendering treat such a value the same as an absent one.
func ParseYAMLMap ¶
ParseYAMLMap decodes YAML into a map with string keys throughout. Every nested map must come out map[string]any, because request bodies are eventually run through json.Marshal in canonicalJSON, which rejects interface-keyed maps. goccy/go-yaml stringifies even non-string keys, so no normalizing pass is needed — TestYAMLKeysAreAlwaysStrings pins that so a dependency bump fails there rather than as an opaque marshal error while hashing.
Types ¶
type Applied ¶
type Applied struct {
Change *Change
// ID is the resource's identifier after the operation, when it has one.
ID string
// Outcome is what was actually done: "created", "updated", "unchanged",
// the kind's Destroy.Past, "already gone" — or "failed" or "interrupted",
// with Err set.
Outcome string
Err error
}
Applied is one resource's outcome, as it happens.
type Applier ¶
type Applier struct {
Client Client
Lock *Lockfile
// Report is told about each resource as soon as it has been applied, so a
// long apply shows progress. Optional.
Report func(Applied)
}
Applier executes a Plan. It persists state after every individual success, because the alternative — writing the lockfile once at the end — loses the IDs of everything created before a mid-run failure, and those resources are then invisible to the next apply and get created a second time.
type Candidate ¶
type Candidate struct {
// Path is the file's absolute path.
Path string
// Markdown reports whether the file has a prose body as well as fields.
Markdown bool
// Prose is the markdown after the frontmatter, empty for a YAML file.
Prose []byte
// Fields are the declared fields: the frontmatter of a markdown file, or
// the whole document of a YAML one. The `type` field is removed once the
// file is classified.
Fields map[string]any
// Name is the default for a resource's `name`: the filename without its
// extension, unless the Schema refines it while classifying.
Name string
// HasFrontmatter tells an empty frontmatter block apart from no
// frontmatter at all: only the latter means the file is prose.
HasFrontmatter bool
// DecodeErr holds a frontmatter or YAML failure. It is fatal for a path the
// user named but merely disqualifying for one we walked into, so it travels
// with the Candidate instead of aborting the read.
DecodeErr error
// contains filtered or unexported fields
}
Candidate is a file read and decoded once. The sniff that decides whether to take a file and the load that builds its Source look at the same bytes, so they cannot disagree and the file is not parsed twice.
type Change ¶
type Change struct {
Key string
Kind Kind
Action Action
// Source is the local definition. Nil for a destroy, whose file is gone.
Source *Source
// Entry is the lockfile record. Nil for a create.
Entry *LockEntry
// Desired is the request body apply will send, with references resolved.
Desired map[string]any
// Remote is the object as the server currently has it, when known.
Remote map[string]any
// Hash is the desired-state fingerprint to record on success.
Hash string
// Diff is the field-level detail shown under an update; nil otherwise.
Diff *Diff
// Reasons explains why this change exists, beyond the field diff — an
// out-of-band edit, a dependency version bump, a recreated resource.
Reasons []string
// Sensitive marks dotted paths whose values must never be printed.
Sensitive map[string]bool
// Replaces is the ID of the tracked resource this create supersedes,
// when one that was gone or archived has to be made again.
Replaces string
// Drift is set when the server's copy no longer matches what we recorded,
// meaning somebody changed it outside this config.
Drift bool
// Blocked carries the reason apply must refuse to proceed.
Blocked error
// Unresolved marks a body containing "(known after apply)" placeholders.
Unresolved bool
// contains filtered or unexported fields
}
Change is one planned operation.
func (*Change) IsSensitive ¶
IsSensitive reports whether printing the whole value at path could disclose a secret: one declared at the path, or anywhere beneath it. List markers ("[]") are ignored on both sides, so "resources[].token" shields "resources" and "resources[]" alike.
func (*Change) SummaryFields ¶
SummaryFields lists the fields that identify this resource in plan output.
type Client ¶
type Client interface {
// Get returns the resource as decoded JSON, or an error wrapping
// ErrNotFound when the server has no such resource.
Get(ctx context.Context, kind Kind, id string) (map[string]any, error)
// Create makes a resource and returns the server's answer, which must
// carry the new resource's string "id". For a payload-backed kind the
// answer may instead be the version just uploaded, with its identifier
// under "version".
Create(ctx context.Context, kind Kind, req Request) (map[string]any, error)
// Update writes to an existing resource and returns the server's answer,
// in the same shape Create returns.
Update(ctx context.Context, kind Kind, id string, req Request) (map[string]any, error)
// Destroy archives or deletes, per the kind's capabilities. An error
// wrapping ErrNotFound means the resource was already gone.
Destroy(ctx context.Context, kind Kind, id string) error
}
Client is the API surface the reconciler needs. It is an interface so the planner and applier can be exercised against a fake without a network.
type Diff ¶
type Diff struct {
Kind DiffKind
// Fields holds the differing members of an object, by name.
Fields map[string]*Diff
// Items holds the differing elements of a list, in display order.
Items []ItemDiff
// Before is the server's value and After the desired one, for the leaf
// kinds that carry them.
Before, After any
}
Diff describes how the desired body differs from the server's copy, as a tree that follows the body's own shape. A nil *Diff means no difference.
type DiffKind ¶
type DiffKind int
DiffKind says what sort of node a Diff is.
const ( // DiffObject and DiffList are interior nodes; see Fields and Items. DiffObject DiffKind = iota + 1 DiffList // DiffValue is a changed leaf: Before became After. DiffValue // DiffText is a changed string, kept apart so a renderer can diff words. DiffText // DiffAdded is a value the server does not have yet (After only). DiffAdded // DiffRemoved is a value being cleared or a list element being dropped // (Before only). DiffRemoved // DiffSensitive is a change whose values must not be shown. DiffSensitive // DiffWriteOnly is a field the server never returns: it will be sent // again, and whether that changes anything is unknowable. DiffWriteOnly )
type FetchedURL ¶
type FetchedURL struct {
// Dir is the local directory the URL was materialized into.
Dir string
URLPin
}
FetchedURL is a URL-referenced resource materialized on local disk.
type Field ¶
type Field struct {
// Computed marks a value the server owns and the config does not set —
// timestamps, IDs, status. It is stripped before comparing, so a missing
// Computed marking shows up as drift that never clears.
Computed bool
// WriteOnly marks a value the server never returns. It cannot be compared,
// and it must never be printed. A "[]" segment in the path means "every
// element of this array".
WriteOnly bool
// Immutable marks a value fixed when the resource is created. Changing it
// needs a replace, which core refuses to do on the user's behalf.
Immutable bool
// Clearable marks a value that deleting from the file should actually
// remove, by sending an explicit null. Without it the field follows the
// API's usual omit-to-preserve rule, which is the safe default: a server
// that fills in its own value would otherwise be fought with forever.
Clearable bool
// Archived marks the server-owned field whose presence means the resource
// was archived out of band. Archiving is a one-way door, so the planner
// offers a replacement rather than an update.
Archived bool
// Shorthand names the sub-field a bare scalar stands for when the API
// accepts either form and always answers with the object:
// `model: x` and `model: {id: x, …}` then compare equal.
Shorthand string
// Summary marks a field worth showing under a create in the plan. The
// whole body would bury the plan in prose; these identify the resource.
Summary bool
// MatchBy names the members that identify an element of this list field,
// most telling first, so a plan can pair elements up before diffing them.
// Unset means id, then name; position is always the last resort.
MatchBy []string
// Metadata marks a string-to-string bag the server patches key by key, so
// core sends null for keys the file no longer declares.
Metadata bool
// Ref marks a field holding a reference to another resource, written in
// the config as a file path and sent to the API as an ID.
Ref *Ref
// Include marks a list field whose entries may name a data file — YAML or
// JSON, by path or glob relative to the declaring file — whose contents
// are spliced into the list where the entry stood. It is for lists of
// hand-written API objects that some other tool generates (a framework
// emitting the custom tool specs it executes, say), so the definition
// stays declarative without being copied in by hand. The file is data,
// not a resource: it gets no lockfile entry, but what it contributes is
// part of the body, so editing it plans an update like any other edit.
Include bool
}
Field describes one field of a resource, after the manner of a Terraform attribute. The zero value is an ordinary field: sent as written, compared as written, preserved when omitted.
type GitHubFetcher ¶
type GitHubFetcher struct {
HTTPClient *http.Client
// Token authenticates against private repositories. Read from GITHUB_TOKEN
// or GH_TOKEN by NewGitHubFetcher.
Token string
// CacheDir holds extracted archives across applies.
CacheDir string
// contains filtered or unexported fields
}
GitHubFetcher fetches resources from github.com tree URLs. Other hosts get a clear error rather than a half-working guess. Build one with NewGitHubFetcher; the zero value has no cache to record fetches in.
func NewGitHubFetcher ¶
func NewGitHubFetcher(cacheDir string) *GitHubFetcher
NewGitHubFetcher returns a fetcher that keeps extracted archives under cacheDir and authenticates with GITHUB_TOKEN, or GH_TOKEN when that is unset.
func (*GitHubFetcher) Fetch ¶
func (g *GitHubFetcher) Fetch(ctx context.Context, rawURL string, pin URLPin) (*FetchedURL, error)
Fetch implements URLFetcher. An unpinned URL is resolved against the GitHub API first; a pinned one goes straight to its recorded commit. Every error it returns starts with rawURL, so the user can tell which reference failed.
type ItemDiff ¶
ItemDiff is one element of a list diff. Before and After are the element's index on the server and in the desired body; -1 means absent on that side. A nil Diff with both indices set is an element that only moved.
type KindSpec ¶
type KindSpec struct {
// Kind is the kind this spec describes.
Kind Kind
// IDPrefix is the leading token of the API's tagged IDs ("agent" in
// "agent_01…"), used to tell an inline ID reference from a file path.
IDPrefix string
// VersionField is the response field holding the resource's version, if it
// has one. Empty means the kind is unversioned, and drift can only be
// detected by fingerprinting the whole object.
VersionField string
// VersionIsInt renders the version as a number rather than a string when
// it is sent back to the API.
VersionIsInt bool
// UpdateNeedsVersion means update requires the caller to echo the current
// version back, under `version` in the request body, which is how the API
// rejects a racing writer.
UpdateNeedsVersion bool
// Destroy is what removal actually does, for plan output: most APIs
// offer archive, delete, or only one of the two.
Destroy Destroy
// Fields describes the resource's fields, one entry per field, keyed by a
// dotted path into the body. Everything the reconciler needs to know about
// a field lives in its entry rather than being spread across parallel
// lists, so one line says everything about a field such as `description`.
Fields Fields
// Build turns a parsed file into a request body. Required for every kind a
// Schema can classify from a file. Leave it nil for a kind whose content
// travels as a Payload: core then refuses to load one from a file, and
// reads a write's version from the version object the upload answers with.
Build func(c *Candidate) (map[string]any, error)
// contains filtered or unexported fields
}
KindSpec is everything core needs to know about one kind. A domain builds one per kind and hands the set over as a Registry; keeping them together means a reader sees every fact about a kind in one place, and adding a kind is one literal rather than an edit in each phase of the engine.
func (KindSpec) IsArchived ¶
IsArchived reports whether the server's copy carries the kind's tombstone. Archiving is a one-way door, so the only way forward is a new resource.
type Loader ¶
type Loader struct {
// contains filtered or unexported fields
}
Loader turns a set of paths into a closed set of Sources: every reference reachable from the named paths is loaded too, so naming one file is enough to apply it together with everything it depends on.
func NewLoader ¶
func NewLoader(registry *Registry, root string, fetcher URLFetcher) *Loader
NewLoader returns a loader for the kinds in registry, keying resources relative to root. A nil fetcher disables URL references.
func (*Loader) Add ¶
Add loads the given paths (files, directories or globs) and everything they reference. The context bounds URL fetches.
func (*Loader) AddKeys ¶
AddKeys loads resources named by lockfile keys, skipping any whose file has been removed — those become deletions, which the planner handles from the lockfile alone.
func (*Loader) Discover ¶
Discover lists the resources under dir that this loader has not loaded, by the same conservative rules a directory walk applies — but without building bodies or following references, so one malformed file cannot hide its neighbours. It is for offering untracked files to the user, who decides; nothing here is added to the loader.
func (*Loader) Pin ¶
Pin seeds what a previous apply recorded for each URL. Without this every apply would re-resolve a branch URL and churn the resource on every push to that branch.
type LockEntry ¶
type LockEntry struct {
Kind Kind `json:"kind"`
ID string `json:"id"`
// Version is the resource version at last apply: an integer for some
// kinds, an epoch string for others, empty for kinds that don't version.
// Kept as a string so both round-trip losslessly.
Version string `json:"version,omitempty"`
// Hash fingerprints the desired state we last sent. A mismatch means the
// local files changed.
Hash string `json:"hash"`
// RemoteHash fingerprints the normalized remote object as it looked right
// after that apply. A mismatch means somebody changed it out of band —
// this is the only drift signal available for kinds with no version field.
RemoteHash string `json:"remote_hash,omitempty"`
// Revision and Subpath are the commit a URL-sourced resource was pinned
// to and where in it the resource lives.
Revision string `json:"revision,omitempty"`
Subpath string `json:"subpath,omitempty"`
// contains filtered or unexported fields
}
LockEntry is the recorded outcome of the last successful apply of one resource. It is the only thing standing between "this file changed" and "the resource changed", so every field here exists to answer a specific question the next plan has to ask.
func (LockEntry) MarshalJSON ¶
MarshalJSON writes the known fields and merges the unknown ones back in. It has a value receiver so that a LockEntry marshals the same way whether or not it is addressable.
func (*LockEntry) UnmarshalJSON ¶
UnmarshalJSON decodes the known fields and keeps any others in unknown.
type Lockfile ¶
type Lockfile struct {
Path string
Resources map[string]*LockEntry
// Origin is where these resources live; nil until the first apply
// records it, and in lockfiles written before it existed.
Origin *Origin
// contains filtered or unexported fields
}
Lockfile is the on-disk state file.
func FindLockfile ¶
FindLockfile walks up from start looking for a lockfile, stopping after the first directory that holds .git. When none is found it returns an empty lockfile rooted at start, which is what a first apply wants.
func LoadLockfile ¶
LoadLockfile reads a lockfile from an explicit path. A missing file is not an error; a malformed one is.
type Origin ¶
type Origin struct {
BaseURL string `json:"base_url,omitempty"`
OrganizationID string `json:"organization_id,omitempty"`
WorkspaceID string `json:"workspace_id,omitempty"`
}
Origin says where a lockfile's resources live: which API host and, when the credentials that wrote it knew, which organization and workspace. It is recorded on the first write so a later run with different credentials can be refused before it mistakes "not yours" for "deleted".
type Payload ¶
type Payload interface {
// Fingerprint identifies the payload's content. Two payloads with the same
// fingerprint are treated as unchanged.
Fingerprint() (string, error)
// Describe is a short human summary for plan output, e.g. "3 files".
Describe() string
}
Payload is content that is not a JSON body — a bundle of files uploaded as multipart, for instance. Core never inspects it; it only fingerprints it for change detection and hands it back to the Client.
type Plan ¶
type Plan struct {
Changes []*Change
Warnings []string
// LockfilePath is where state will be written.
LockfilePath string
// LockfileExisted reports whether state was found or is being started.
LockfileExisted bool
// contains filtered or unexported fields
}
Plan is the full set of operations, in execution order.
func (*Plan) Change ¶
Change returns the planned change for a resource key, or false when the plan has none.
type Planner ¶
type Planner struct {
// Registry supplies the kinds this planner understands.
Registry *Registry
Client Client
Lock *Lockfile
// Force proceeds even when a resource changed out of band.
Force bool
// Prune destroys resources that are in the lockfile but no longer on disk.
Prune bool
// Concurrency bounds parallel reads of remote state. Zero or less means 8.
Concurrency int
// contains filtered or unexported fields
}
Planner diffs local definitions against the API and produces a Plan.
type Ref ¶
type Ref struct {
// To restricts what the reference may name.
To []Kind
// List marks a field holding several references rather than one.
List bool
// As renders a resolved target into the body. Nil writes the bare ID.
As func(Target) any
}
Ref describes a reference field: what it may point at, and how the resolved target is written into the request body.
type RefSlot ¶
RefSlot is a Ref located at a dotted path into the body. NewRegistry derives one for each field in KindSpec.Fields whose Ref is set. References are declared, never sniffed: sniffing would turn a system prompt that mentions "./notes" into a dependency.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is the set of kinds a reconciler works with, in an order where a kind's dependencies come before it.
func NewRegistry ¶
NewRegistry builds a registry. Specs must be given dependencies-first: plan output, tie-breaking and destroy ordering all rely on it, and TestKindOrderIsTopological in the domain package is what keeps it honest.
func (*Registry) KindForID ¶
KindForID maps a tagged ID back to its kind. It returns false for anything that does not look like an ID, which is how a reference is told apart from a file path. Too strict fails loudly (an ID treated as a path matches nothing on disk); too loose fails silently (a path sent to the API as an ID).
An ID may carry one lowercase label between the prefix and the suffix ("agent_label_0123…"); that is still an ID.
func (*Registry) LockfileName ¶
LockfileName is the state file's conventional name, as the domain has it.
type Schema ¶
type Schema interface {
// Classify names the kind a parsed file declares. Returning ("", nil) means
// "not a resource": a directory walk must tolerate that, since a repository
// is full of prose and CI config. walked reports whether the file was found
// by walking rather than named outright, which is usually the difference
// between skipping something and complaining about it. Classify may read
// the `type` field to decide; core drops that field from c.Fields
// afterwards, so a Build never sees it.
Classify(c *Candidate, walked bool) (Kind, error)
// IsResourceDir reports whether a directory is itself a resource rather
// than a folder of them. It is asked often, so it must be cheap — a stat,
// not a walk.
IsResourceDir(dir string) (Kind, bool)
// LoadDir loads a directory IsResourceDir accepted.
LoadDir(dir string) (Kind, map[string]any, Payload, error)
// ResourceDirOf maps a file that stands in for a directory-backed resource
// to that directory — a marker file to its folder.
ResourceDirOf(path string) (string, bool)
// LockfileName is the state file's conventional name. A directory walk
// skips it, and discovery looks for it walking up.
LockfileName() string
}
Schema is how a domain describes its file layout. Core reads files, resolves references between them and reconciles the result; it has no opinion about which file means what, so every convention — a directory name, an extension, a marker file — lives behind this interface rather than as a flag core has to know the meaning of.
type Source ¶
type Source struct {
// Key is the stable identity used in the lockfile and in plan output:
// a root-relative slash path prefixed with "./", or the verbatim URL for
// a URL-sourced resource.
Key string
Kind Kind
// Path is the absolute path of the defining file. Empty when the resource
// came from a URL.
Path string
// Dir is set when the resource is declared by a directory rather than a
// file.
Dir string
// Payload is the non-JSON content of a directory-backed resource.
Payload Payload
// URL is set when the resource was fetched from a URL rather than read
// from the working tree, and Pin records what it resolved to.
URL string
Pin URLPin
// Body is the declared request body, minus apply-only directives.
Body map[string]any
}
Source is one resource as declared on disk, or fetched from a URL. Body holds the declared fields verbatim; references inside it are still paths at this stage and get resolved to IDs later, once the graph is ordered.
type Target ¶
type Target struct {
Kind Kind
ID string
// Version is the referenced resource's version in its JSON form: an
// integer for some kinds, an epoch string for others. Nil when there is
// none to pin yet.
Version any
// Pinned is false when the user asked to always track latest, in which
// case EncodeObject omits the version and lets the server resolve it.
Pinned bool
// Versioned reports whether the target's kind has a version at all;
// pinning is meaningless for one that does not.
Versioned bool
// Known is false at plan time for a resource that does not exist yet, so
// its ID and version are "known after apply".
Known bool
}
Target is a resolved reference: what the referenced resource became after (or, at plan time, is predicted to become after) apply.
type URLFetcher ¶
type URLFetcher interface {
// Fetch downloads rawURL. When pin carries a revision the fetcher must
// return exactly that rather than whatever the URL's branch currently
// points at.
Fetch(ctx context.Context, rawURL string, pin URLPin) (*FetchedURL, error)
}
URLFetcher materializes a URL-referenced resource into a local directory and reports the immutable revision it resolved to.
type URLPin ¶
type URLPin struct {
// Revision is the immutable identifier the URL resolved to — a commit SHA
// for GitHub.
Revision string
// Subpath is where in that revision the resource lives. A tree URL does
// not say where the branch name ends and the path begins; this records
// the answer so a pinned fetch need not rediscover it.
Subpath string
}
URLPin is what the lockfile remembers about a URL so a later apply lands on the same content without asking the host again.