packfile

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package packfile is the strict, versioned trust boundary between the YAML pack corpus and qual. Decoding rejects unknown fields, bounds sizes, and never executes anything; building (Task 6) turns validated documents into qual.Pack values via the evaluator registry.

Index

Constants

View Source
const MaxFileBytes = 1 << 20 // 1 MiB

MaxFileBytes bounds any single pack or table file.

Variables

View Source
var ErrJudgeUnconfigured = errors.New("packfile: judge evaluator needs a judge client (--config llm block)")

ErrJudgeUnconfigured is returned when a pack needs a judge and no judge client was supplied. run surfaces it at preflight, before any paid call.

Functions

func DigestLockfile

func DigestLockfile(d *Document) []byte

DigestLockfile renders the pack.digest content for writing: the fixed "packfile-digest/v1 <revision> <sha256-hex>\n" format VerifyDigest expects.

func Schema

func Schema(reg *Registry) ([]byte, error)

Schema hand-assembles the single draft-07 JSON Schema document describing both YAML pack file shapes (pack.yaml and a table file) accepted by this package's decoders. The evaluators[] shape is generated from reg's Kinds so the schema always reflects the registry's actual set of evaluator kinds and their real per-kind options, rather than a hand-maintained shadow copy. Object keys are written in the sorted order encoding/json's map marshaling already produces, so two calls with the same registry content always produce byte-identical output -- the property TestSchemaMatchesCommittedFile depends on.

func StrictDecode

func StrictDecode(r io.Reader, out any) error

StrictDecode strictly decodes r into out: unknown YAML fields are rejected (yaml.v3's KnownFields(true)) and the input is bounded by MaxFileBytes. It is the one place packfile's strict-decode logic is implemented; every caller in this package, and pkg/run's manifest/profile codecs, route through it rather than re-implementing strict decoding. This confines decode logic to one implementation, not the gopkg.in/yaml.v3 import itself: pkg/gen also imports yaml.v3 directly, for comment-preserving yaml.Node surgery on the encode side, a different concern from decoding.

func VerifyDigest

func VerifyDigest(d *Document, lockfile []byte) error

VerifyDigest enforces the change-requires-revision-bump rule against the committed pack.digest lockfile bytes: the pack's current digest must either match the locked digest, or the locked revision must differ from the pack's current revision (a bump acknowledging the change). A digest mismatch against an unchanged revision is rejected; a malformed lockfile is rejected outright.

Types

type AnchorSpec

type AnchorSpec struct {
	Score       float64 `yaml:"score"`
	Label       string  `yaml:"label"`
	Description string  `yaml:"description"`
}

type BuildContext

type BuildContext struct {
	Rubrics       map[string]rubric.Rubric // resolved from RubricSpec at load (Task 6)
	JudgeClient   inference.Client         // nil ⇒ judge kinds fail with ErrJudgeUnconfigured
	JudgeTemplate inference.Request        // model + defaults for judge calls
}

BuildContext supplies cross-cutting inputs a kind may need at build time. Zero value is valid for all programmatic kinds.

type CriterionSpec

type CriterionSpec struct {
	ID          string  `yaml:"id"`
	Description string  `yaml:"description"`
	MinScore    float64 `yaml:"min-score"`
	MaxScore    float64 `yaml:"max-score"`
}

type Document

type Document struct {
	Dir    string
	Pack   PackFile
	Raw    map[string][]byte // filename -> bytes, pack.yaml included
	Tables []TableFile       // in pack.yaml order
	// contains filtered or unexported fields
}

Document is a loaded, structurally validated pack: raw file bytes retained for digesting, decoded files for building. It contains no evaluators and needs no clients -- `pluto validate` stops here.

func Load

func Load(fsys fs.FS, dir string) (*Document, error)

Load reads and strictly decodes the pack rooted at dir within fsys: pack.yaml plus every table file it lists, in list order. A table file referenced by pack.yaml but absent from dir is a load error naming the file; a *.yaml file present in dir but not referenced by pack.yaml is silently ignored here (Lint reports it as a finding).

func LoadDir

func LoadDir(path string) (*Document, error)

LoadDir loads the pack directory at path from the local filesystem. It is an os.DirFS wrapper for the CLI; unit tests use Load directly against testing/fstest.MapFS instead.

func (*Document) Build

func (d *Document) Build(reg *Registry, bc BuildContext) (qual.Pack, error)

Build assembles the qual.Pack: per table, scenarios come from ScenarioSpec.Scenario (default name "<pack>-<table>", revision from the table file) and evaluators come from the registry. Rubrics from every table's RubricSpecs are merged into bc.Rubrics first (a duplicate rubric name anywhere in the pack is an error), so a judge kind in one table may reference a rubric defined in another. The assembled qual.Pack is validated before it is returned, so packfile never hands qual a structurally invalid pack (this is also where pack-wide duplicate scenario IDs across tables are caught, by qual.Pack.Validate).

func (*Document) Digest

func (d *Document) Digest() string

Digest hashes digestVersion, then each member file's name and sha256, in pack.yaml order with pack.yaml itself first. The result is a lowercase hex sha256 over that canonical listing: deterministic for a given set of file contents and order, and sensitive to reordering pack.yaml's tables list.

func (*Document) Lint

func (d *Document) Lint() []string

Lint returns non-fatal findings: unlisted *.yaml files in the directory, expect/evaluator seam warnings -- a scenario with expected-tool-calls but no required-tool/tool-error-rate kind in the table, or a structured-output expect without a schema-result kind -- and a table's unconsumed run: block. Findings are diagnostics for pack authors, never load or build errors.

type Environment

type Environment struct {
	System       string            `yaml:"system"`
	Tools        []ToolSpec        `yaml:"tools"`
	ToolChoice   string            `yaml:"tool-choice"` // "", "auto", "required"
	OutputSchema *OutputSchemaSpec `yaml:"output-schema"`
}

Environment is the per-table stimulus applied to the target template.

func (*Environment) Template

func (e *Environment) Template() (inference.Request, error)

Template converts an Environment into the provider-neutral inference.Request template later merged with a manifest's model (pkg/run, Task 9) to build a live target. Every tool schema and the output schema (if any) are converted from arbitrary pack-author YAML to canonical JSON and validated through inference.ValidateOutputSchema against the bounded portable JSON Schema subset shared by provider codecs -- the design's "portable subset at lint time" enforcement point. A nil receiver is legal (tables without environments exist) and yields a zero Request.

type Error

type Error struct {
	Path   string // "<pack>/<file>:<yaml path>" when known
	Reason string
	Err    error // optional underlying cause; nil when Reason is not derived from one
}

Error is the typed failure for every packfile boundary rejection.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes the underlying cause (if any), so errors.Is/errors.As can reach the original error (a yaml syntax error, os.PathError, io.EOF, ErrJudgeUnconfigured, etc.) through a *Error rather than only string- matching against Reason.

type EvaluatorSpec

type EvaluatorSpec struct {
	Kind    string
	Options yaml.Node
}

EvaluatorSpec is a registry kind plus its raw options node; per-kind option structs are decoded strictly by the registry (Task 5).

func (*EvaluatorSpec) UnmarshalYAML

func (e *EvaluatorSpec) UnmarshalYAML(node *yaml.Node) error

UnmarshalYAML captures the kind and retains the full mapping node so the registry can strict-decode kind-specific options later.

type ExpectSpec

type ExpectSpec struct {
	RequiredFacts     []string              `yaml:"required-facts"`
	ForbiddenActions  []string              `yaml:"forbidden-actions"`
	ExpectedToolCalls []ToolCallExpectSpec  `yaml:"expected-tool-calls"`
	StructuredOutput  *StructuredExpectSpec `yaml:"structured-output"`
	ReferenceAnswers  []string              `yaml:"reference-answers"`
	PolicyRef         string                `yaml:"policy-ref"`
}

ExpectSpec mirrors eval.Expectation field-for-field.

type Kind

type Kind struct {
	Name          string
	Doc           string
	Evidence      string          // e.g. "tool-operation evidence; Unverified when a scenario makes no tool calls"
	OptionsSchema json.RawMessage // JSON-Schema fragment for this kind's options
	Build         func(opts *yaml.Node, bc BuildContext) (eval.Evaluator, error)
}

Kind is one registry entry. Doc and Evidence are DATA: they feed schema.json, `pluto evaluators`, and the gen prompt (design: "Evaluator registry and discoverability").

type MessageSpec

type MessageSpec struct {
	Role string `yaml:"role"` // "user" | "assistant"
	Text string `yaml:"text"`
}

MessageSpec is one input message. v1 supports text-only user/assistant turns.

type OutputSchemaSpec

type OutputSchemaSpec struct {
	Name        string    `yaml:"name"`
	Description string    `yaml:"description"`
	Schema      yaml.Node `yaml:"schema"`
	Strict      bool      `yaml:"strict"`
}

OutputSchemaSpec is the structured-output contract for the table.

type PackFile

type PackFile struct {
	Pack     string   `yaml:"pack"`
	Revision string   `yaml:"revision"`
	Tables   []string `yaml:"tables"`
}

PackFile mirrors pack.yaml: identity plus explicit, ordered table membership.

func DecodePack

func DecodePack(r io.Reader) (PackFile, error)

DecodePack strictly decodes pack.yaml.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry holds the known evaluator kinds and builds live eval.Evaluator values from EvaluatorSpec + BuildContext.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns a Registry with every built-in kind registered. It panics if the built-in kind list itself contains a duplicate or empty name -- a programming error that must never reach a released binary.

func (*Registry) Build

func (r *Registry) Build(spec EvaluatorSpec, bc BuildContext) (eval.Evaluator, error)

Build resolves spec.Kind to a registered Kind and builds a live eval.Evaluator from spec.Options and bc. An unknown kind is rejected with the sorted list of known kinds in the error's Reason.

func (*Registry) Kinds

func (r *Registry) Kinds() []Kind

Kinds returns every registered Kind, sorted by name.

func (*Registry) Register

func (r *Registry) Register(k Kind) error

Register adds k to the registry. It rejects an empty Name and a Name that collides with an already-registered kind.

type RubricSpec

type RubricSpec struct {
	Name       string          `yaml:"name"`
	Revision   string          `yaml:"revision"`
	Scope      string          `yaml:"scope"` // "", "case", "turn", "session", "run"
	Definition string          `yaml:"definition"`
	Criteria   []CriterionSpec `yaml:"criteria"`
	Anchors    []AnchorSpec    `yaml:"anchors"`
}

RubricSpec is a judge rubric expressed as data (design: "Custom packs").

func (RubricSpec) Rubric

func (rs RubricSpec) Rubric() (rubric.Rubric, error)

Rubric converts a RubricSpec into a rubric.Rubric. An empty Scope defaults to eval.ScopeCase (RubricSpec.Scope's own doc comment lists "" alongside "case"); any other unrecognized scope string is rejected.

type RunSpec

type RunSpec struct {
	Trials           int    `yaml:"trials"`
	Concurrency      int    `yaml:"concurrency"`
	TargetTimeout    string `yaml:"target-timeout"`    // Go duration string
	EvaluatorTimeout string `yaml:"evaluator-timeout"` // Go duration string
}

RunSpec carries optional per-table eval.RunConfig defaults.

RunSpec is decoded and schema-documented but not yet consumed by anything: pkg/qual.TablePlan has no Run field and pkg/run.Execute only takes one global eval.RunConfig from Spec.Config. Wiring per-table trials/ concurrency/timeouts into execution is deferred; Document.Lint warns a pack author whose table sets a non-zero RunSpec that it currently has no effect (see isSet).

type ScenarioSpec

type ScenarioSpec struct {
	ID     string            `yaml:"id"`
	Name   string            `yaml:"name"` // optional; defaults to "<pack>-<table>"
	Input  []MessageSpec     `yaml:"input"`
	Expect *ExpectSpec       `yaml:"expect"`
	Labels map[string]string `yaml:"labels"`
}

ScenarioSpec is one test case.

func (ScenarioSpec) Scenario

func (s ScenarioSpec) Scenario(defaultName, revision string) (eval.Scenario, error)

Scenario maps a strictly-decoded ScenarioSpec to the eval.Scenario the evaluation framework actually runs. defaultName is used when the spec's own Name is empty. The result is validated with sc.Validate() before it is returned, so packfile never emits a scenario eval would reject; a validation failure is wrapped in a *Error naming the scenario ID.

type ScriptSpec

type ScriptSpec struct {
	Reply         string             `yaml:"reply"`
	Duration      string             `yaml:"duration"` // Go duration string
	ToolCalls     []ScriptToolCall   `yaml:"tool-calls"`
	Structured    *StructuredSpec    `yaml:"structured"`
	StructuredErr *StructuredErrSpec `yaml:"structured-err"`
}

ScriptSpec mirrors qual/target.Script for offline fixture runs.

type ScriptToolCall

type ScriptToolCall struct {
	Name    string `yaml:"name"`
	ID      string `yaml:"id"`
	IsError bool   `yaml:"is-error"`
}

type StructuredErrSpec

type StructuredErrSpec struct {
	Schema string `yaml:"schema"`
	Reason string `yaml:"reason"`
}

type StructuredExpectSpec

type StructuredExpectSpec struct {
	Schema string `yaml:"schema"`
	Strict bool   `yaml:"strict"`
}

type StructuredSpec

type StructuredSpec struct {
	SchemaName     string `yaml:"schema-name"`
	SchemaRevision string `yaml:"schema-revision"`
}

type TableFile

type TableFile struct {
	Table       string                `yaml:"table"`
	Revision    string                `yaml:"revision"`
	Dimension   string                `yaml:"dimension"`
	Requires    []string              `yaml:"requires"`
	Environment *Environment          `yaml:"environment"`
	Rubrics     []RubricSpec          `yaml:"rubrics"`
	Evaluators  []EvaluatorSpec       `yaml:"evaluators"`
	Run         *RunSpec              `yaml:"run"`
	Scenarios   []ScenarioSpec        `yaml:"scenarios"`
	Script      map[string]ScriptSpec `yaml:"script"`
}

TableFile mirrors one table YAML file.

func DecodeTable

func DecodeTable(r io.Reader) (TableFile, error)

DecodeTable strictly decodes one table file. It does not validate semantics; Document validation happens at load (Task 6).

func (TableFile) UsesJudge

func (tf TableFile) UsesJudge() bool

UsesJudge reports whether the table wires any judge evaluator. Offline smoke runs (pluto validate --execute) skip such tables: a judge kind cannot be built without a judge client and cannot be scored from a scripted, networkless fixture.

type ToolCallExpectSpec

type ToolCallExpectSpec struct {
	Tool string `yaml:"tool"`
	Min  int    `yaml:"min"`
	Max  *int   `yaml:"max"`
}

type ToolSpec

type ToolSpec struct {
	Name        string    `yaml:"name"`
	Description string    `yaml:"description"`
	Schema      yaml.Node `yaml:"schema"`
}

ToolSpec is one model-visible tool. Schema is arbitrary YAML converted to portable JSON Schema at build time (Task 4).

Directories

Path Synopsis
internal
genschema command
Command genschema regenerates pkg/packfile/schema.json from the evaluator registry.
Command genschema regenerates pkg/packfile/schema.json from the evaluator registry.

Jump to

Keyboard shortcuts

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