Documentation
¶
Overview ¶
Package manifest reads and writes the skills-manifest.json a verify pass emits: a machine-readable summary of a verified skill tree that downstream tools consult instead of rediscovering it.
Build and Marshal produce one; Parse and Diff consume one. Diff answers the question a manifest is kept for — which skills have changed since it was written — so a caller can reprocess only those. Every function here is pure; the caller does the file I/O.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Axes ¶ added in v0.19.0
type Axes struct {
// Skill is true when SKILL.md's hash differs, or is unknown on either side.
Skill bool `json:"skill"`
// TestPrompts is true when the test-prompts file appeared, disappeared, or
// changed content. It is false when both sides have no test prompts at all —
// absence on both sides is not a change, and reporting it as one would make
// every skill without prompts permanently interesting.
TestPrompts bool `json:"test_prompts"`
}
Axes reports which of a skill's two files moved.
The zero value says neither, which is the honest reading of a location nobody examined — and never appears in a Delta, because Diff records an entry only for a location it placed in Changed.
type Delta ¶ added in v0.11.0
type Delta struct {
Added []string // present in cur, absent from base
Removed []string // present in base, absent from cur
Changed []string // in both, and not known to be identical
Unchanged []string // in both, with equal known hashes
// ChangedAxes says *what* changed, for each location in Changed. A location
// absent from this map is in Changed for a reason the axes do not cover.
//
// A map keyed by a subset of Changed, rather than a fifth slice or a richer
// Changed element. A fifth slice would break the totality promise above — a
// location whose prompts changed is also a location that changed — and turning
// Changed into a slice of structs would be a breaking change to a kernel type
// with four consumers, for an ergonomic gain. The subset relation is asserted by
// a test rather than left to this comment.
ChangedAxes map[string]Axes `json:"changed_axes,omitempty"`
}
Delta is the difference between two manifests, as tree-relative skill locations.
It is total: every location present in either manifest appears in exactly one of the four slices, so a caller can report "skipped N unchanged" without a second pass and the four lengths sum to the size of the union. Each slice is sorted.
func Diff ¶ added in v0.11.0
Diff reports how the skills in cur differ from those recorded in base.
Skills are matched on location, not slug. A slug is not unique: skill.DiscoverRoots scans several runtime roots, so .claude/skills/foo and .cursor/skills/foo are two distinct skills sharing the slug "foo", and matching on slug would silently collapse them into one. The location is taken relative to each manifest's own Tree, so a tree recorded as "." matches the same tree recorded by absolute path — otherwise two runs that spelled --tree differently would report every skill as both added and removed.
An entry whose hash is unknown on either side is reported as Changed, never as Unchanged. Hash is omitempty and a writer leaves it empty when the skill could not be loaded, so treating it as unchanged would silently skip a skill that was never successfully hashed. The same applies when a location appears twice with disagreeing hashes: neither can be trusted, so it counts as changed.
Only Tree and Skills are read. Tool and StructureVerified are ignored, so a caller can assemble cur as a plain struct literal from a fresh scan rather than inventing a value for a field that has no meaning before verification has run.
Ensures: the four Delta slices partition the union of both manifests' locations,
and ChangedAxes is keyed by exactly the members of Changed; it is pure and does not mutate either argument.
A skill whose test prompts changed is Changed even when its prose did not, which is a behaviour change: before this, a manifest recorded that a test-prompts file existed and nothing about its content, so the pair could drift apart with every gate passing.
func (*Delta) Stale ¶ added in v0.11.0
Stale returns the locations that need reprocessing: those added since base plus those whose content changed, sorted.
This is the question a caller asks when a manifest is used as a skip list, so the answer lives here rather than in each caller. Two callers unioning the same two fields themselves would be the same design decision written down twice.
The receiver is a pointer only to avoid copying the four slice headers; Stale does not mutate the Delta, and Delta has no other methods.
type Manifest ¶
type Manifest struct {
Tool string `json:"tool"` // the emitting tool, e.g. "exegesis"
Tree string `json:"tree"` // the verified tree path
StructureVerified bool `json:"structure_verified"` // true iff every gate passed
Skills []Skill `json:"skills"`
// EdgesRecorded says whether the producer populated Skill.Edges at all.
//
// **The question is about the manifest, not about any skill, which is why it lives
// here.** Per-skill nil cannot answer it: encoding/json omits a nil map and an empty
// one alike, so a skill that declares no edges and a skill whose edges were never
// read are the same bytes. A consumer needs to tell "compared against nothing" from
// "compared and found nothing" -- the timeseries.Verdict.Compared distinction -- and
// inferring it from "every skill has no edges" would be wrong for the one tree that
// genuinely has none.
//
// False on a manifest written before Edges existed, which is the case it is for. A
// producer that populates Edges and forgets this flag fails **closed**: the consumer
// reads the graph as unavailable and declines to report, rather than reporting a
// silent "no changes" against a baseline it never had.
EdgesRecorded bool `json:"edges_recorded,omitempty"`
// Examined is how many skills the producer looked at.
//
// StructureVerified answers "did every gate pass" and nothing else, so a tree holding no
// skills produced `structure_verified: true` -- vacuously, no gate failed -- and a
// consumer gating on that boolean shipped on a verdict about nothing. skillsaw's
// `verified` command is exactly that consumer.
//
// **It buys legibility, not information, and the doc should not pretend otherwise.**
// len(Skills) already carries this number, and skillsaw already prints it while gating
// on the boolean beside it. The field exists because a reader checking a verdict does
// not think to check an array length -- which is how the defect shipped -- not because
// the count was unavailable.
//
// **Absent and zero are the same bytes, and the ambiguity is resolved from data rather
// than by a second field.** A producer that examined nothing lists nothing, so
// `Examined == 0 && len(Skills) > 0` can only be a manifest written before this existed.
// That inference is what buys the plain int over a *int or an ExaminedRecorded
// companion, and TestExaminedZeroWithSkillsMeansAnOlderManifest keeps it a checked claim.
//
// Set by the producer after Build, the way EdgesRecorded is: Build takes what every
// manifest has, and a producer that forgets this leaves it zero -- which reads as
// "examined nothing" and fails closed, the same direction EdgesRecorded chose.
Examined int `json:"examined,omitempty"`
}
Manifest is the top-level skills-manifest.json document.
func Build ¶
Build assembles a Manifest, sorting skills by slug so output is deterministic. tool names the emitting tool; verified reflects whether every gate passed across all skills. It is pure and does not mutate the input slice.
func Parse ¶ added in v0.11.0
Parse decodes a skills-manifest.json document.
A document with no "tool" field is rejected. Unmarshalling arbitrary JSON into a Manifest otherwise succeeds and yields a zero value, so aiming a --manifest flag at the wrong file would read as an empty tree rather than as a mistake — and a diff against an empty tree reports every skill as added, which looks like a real answer. Every manifest written by a tool in this family sets "tool", so its absence identifies the wrong-file case without rejecting any genuine manifest.
Unknown fields are ignored, so a manifest from a newer tool still reads.
Ensures: the result round-trips a Marshal of the same manifest; it is pure.
type Skill ¶
type Skill struct {
Slug string `json:"slug"`
Dir string `json:"dir"`
Hash string `json:"sha256,omitempty"` // first-16 sha256 of SKILL.md
TestPrompts string `json:"test_prompts,omitempty"` // path if present, else ""
// TestPromptsHash is the first-16 sha256 of the test-prompts file, or "" when
// there is none.
//
// Without it a manifest records that a skill has test prompts and nothing about
// what they say, so a SKILL.md can be rewritten while its behavioural assertions
// still describe the previous version and every gate in the family passes. The
// only thing comparing versions is Diff, and it had nothing to compare on.
//
// Empty means **absent**, not unknown. That distinction is load-bearing here in a
// way it is not for Hash: Diff treats an empty Hash as unknown-therefore-changed,
// and copying that rule would report every skill without test prompts as changed
// on every run.
TestPromptsHash string `json:"test_prompts_sha256,omitempty"`
// Edges is the skill's related-skills graph: edge kind to the slugs it points at,
// sorted. Empty when the skill declares none.
//
// It exists for the one question a manifest could not answer about a tree it no longer
// has: what the graph looked like. Whether a skill was orphaned by a change needs the
// baseline's edges, and Diff can say a body moved but never what it said. A consumer
// comparing two checkouts should read both trees instead -- one parser, one version, no
// drift -- and this is for the case where the baseline is a published artifact and the
// checkout is gone.
//
// map[string][]string rather than a related.Edge slice, deliberately: this package is
// stdlib-only, and the one other kernel package that gave that up recorded it as a cost.
// related.Kind is a string type, so a consumer converts without a decoder. The rationale
// on each edge is dropped -- the graph question needs kind and target, and prose would
// bloat a file kept for hashes.
//
// **Recorded, not diffed.** Edges live in SKILL.md, so any edge change already moves
// Hash and surfaces as Axes.Skill. Feeding them to axes as well would report one change
// on two axes and make every graph edit look like two.
Edges map[string][]string `json:"edges,omitempty"`
}
Skill is one verified skill's entry.