Documentation
¶
Overview ¶
Package sweep is the one rule set for a Compose parameter sweep (types.SweepSpec, the `sweep` slot of a ComposedRequest) and for the item fan-out that reuses its axes and substitution (types.ForEach, the `for_each` key on a window, feature or attribute — items.go, ExpandItems).
It is NO-EXECUTE and sits below both callers that must agree on it: the runtime (internal/service, Compose / ComposeParallel) and predict (internal/descriptor, PredictCompose), which may not import the engine (TestPredictNoExecutionImports). It therefore imports only the standard library, errors, types and — for expansion — the import-fenced internal/template (TestSweep_ImportBoundary).
Validate is the structural check every surface runs before any slot is expanded. Its rules (the contract):
- axes is non-empty;
- every axis name is an identifier ([A-Za-z_][A-Za-z0-9_]*), unique within the sweep;
- every axis carries a non-empty values list of scalars — a number (json.Number, any integer kind, a finite float), a string or a boolean; null, objects and arrays are refused;
- mode is empty, grid or zip; zip needs every axis the same length;
- request is a JSON object; overlays, when set, is a JSON array;
- rank, when set, names a non-empty `by` of dotted segments with none empty; order is empty, asc or desc; top, when set, is at least 1.
Checks run in that order, axes in declared order, and the first fault is returned — a malformed non-empty `by` as PULSE_SWEEP_RANK_PATH (reason syntax, see ParseRankPath), every other fault as PULSE_SWEEP_INVALID with details `field` (the sweep- rooted path of the offending value), `reason`, `axis` (the axis name) where one is involved and, where they apply, `value` / `valid`. Whether every axis is referenced and every placeholder names an axis needs the substitution scan, so expansion checks it.
Index ¶
- Constants
- func Count(spec *types.SweepSpec) int
- func DerivedCount(req *types.Request) int
- func HasForEach(req *types.Request) bool
- func ParseRankPath(by string) ([]string, *errors.CodedError)
- func Rank(r *types.SweepRank, labels []string, responses []*types.Response) ([]types.RankEntry, error)
- func Validate(spec *types.SweepSpec) *errors.CodedError
- type Expansion
- type FanOut
Constants ¶
const ( // ReasonAxisUnreferenced: an axis is named by no placeholder in the // request body, the authored label pattern or the overlays — every // slot it adds would run the same request. ReasonAxisUnreferenced = "axis_unreferenced" // ReasonPlaceholderUnknown: a `{{name}}` token, `{"$var": "name"}` // marker or `$when` guard names no axis. ReasonPlaceholderUnknown = "placeholder_unknown" // ReasonPlaceholderInvalid: placeholder syntax the substitution // grammar refuses — an unterminated or empty `{{}}`, `{{` inside an // object key, a `$var` marker whose name is not a string, a `$when` // guard on the request root. ReasonPlaceholderInvalid = "placeholder_invalid" // ReasonRequestDecode: a substituted request body does not decode // strictly into a Request — an unknown key (named under // `unknown_field`) or a value of the wrong JSON type. ReasonRequestDecode = "request_decode" // ReasonRequestLabelSet: the request body sets `label`; a sweep // slot's label comes from the sweep `label` pattern. ReasonRequestLabelSet = "request_label_set" // ReasonOverlaysDecode: a substituted overlays array does not decode // into compose overlay specs. ReasonOverlaysDecode = "overlays_decode" // ReasonLabelEmpty: the label pattern rendered to the empty string // for a slot. ReasonLabelEmpty = "label_empty" )
The reason values expansion adds to PULSE_SWEEP_INVALID.
const ( // ReasonItemDecode: a substituted item does not decode strictly into // its slot type — an unknown key (named under `unknown_field`) or a // value of the wrong JSON type. ReasonItemDecode = "item_decode" // ReasonNestedInSweep: a compose sweep body carries an item with a // `for_each`. A sweep body is substituted once per slot; a second // fan-out inside it is refused rather than expanded twice. ReasonNestedInSweep = "nested_in_sweep" )
The reason values item expansion adds to PULSE_FOR_EACH_INVALID, beside the structural axis reasons (ReasonAxesEmpty … ReasonZipLengthMismatch) and the placeholder reasons (ReasonAxisUnreferenced, ReasonPlaceholderUnknown, ReasonPlaceholderInvalid) it shares with the compose sweep.
const ( // RankReasonSyntax: `by` is not dotted segments — an empty segment // (a leading, trailing or doubled `.`). Refused by predict and the // runtime before any slot runs. RankReasonSyntax = "syntax" // RankReasonMissing: a segment names no key of an object, no list // element, or descends through a null. A refusal when the path // resolves in no sweep slot; a warning on the slot otherwise. RankReasonMissing = "missing" // RankReasonNotNumber: the path ends on a string, boolean, object or // list, or a segment descends into a scalar. Always a refusal. RankReasonNotNumber = "not_number" // RankReasonAmbiguous: a segment on a list matches more than one // element by `name` / `label`, or matches an element whose `name` // and `label` differ. Always a refusal. RankReasonAmbiguous = "ambiguous" // RankReasonNull: the path ends on null — an undefined figure (NaN // on the wire). A warning on the slot; never ranked last. RankReasonNull = "null" )
The reason values PULSE_SWEEP_RANK_PATH carries under details.reason — on the refusal, and on the warning a slot left out of the ranking carries.
const ( ReasonAxesEmpty = "axes_empty" ReasonAxisNameInvalid = "axis_name_invalid" ReasonAxisNameDuplicate = "axis_name_duplicate" ReasonValuesEmpty = "values_empty" ReasonValueNotScalar = "value_not_scalar" ReasonModeUnknown = "mode_unknown" ReasonZipLengthMismatch = "zip_length_mismatch" ReasonRequestMissing = "request_missing" ReasonRequestNotObject = "request_not_object" ReasonOverlaysNotArray = "overlays_not_array" ReasonRankByEmpty = "rank_by_empty" ReasonRankOrderUnknown = "rank_order_unknown" ReasonRankTopInvalid = "rank_top_invalid" )
The reason values PULSE_SWEEP_INVALID carries under details.reason.
Variables ¶
This section is empty.
Functions ¶
func Count ¶
Count returns how many slots a structurally valid spec expands to: the product of the axis lengths under grid, the common axis length under zip, 0 for a nil spec or one without axes. A product past math.MaxInt saturates there, so a limit check on it never wraps.
func DerivedCount ¶
DerivedCount is the number of window, feature and attribute items req carries once every for_each is expanded: one per plain item, the axis product (grid) or common length (zip) per for_each item. It saturates at math.MaxInt. Structure is not checked; ExpandItems checks it before it calls its preflight with this figure.
func HasForEach ¶
HasForEach reports whether any window, feature or attribute item of req carries a for_each.
func ParseRankPath ¶
func ParseRankPath(by string) ([]string, *errors.CodedError)
ParseRankPath splits a rank `by` path into its dotted segments, refusing an empty segment as PULSE_SWEEP_RANK_PATH (reason syntax). An empty `by` is Validate's PULSE_SWEEP_INVALID, not this.
func Rank ¶
func Rank(r *types.SweepRank, labels []string, responses []*types.Response) ([]types.RankEntry, error)
Rank orders the finished sweep slots by the number each one carries at r.By and returns the ranking (never nil). labels and responses are the SWEEP slots only, index-aligned, in expansion order; explicit `requests` are never ranked.
Each slot is read in its wire form — the response marshalled as JSON (non-finite floats written as null) BEFORE any `return` shaping — so a path names exactly what a reader of an unshaped response sees. One exception: a final segment the wire lacks because `omitempty` dropped a computed figure that is exactly 0 is read off the typed Response and ranks as 0 (rank_typed.go); an uncomputed figure stays missing. Path grammar: dotted segments; on an object a segment is a key, on a list it selects the one element whose `name` or `label` equals it (never an index); the path must end on a number.
A slot whose path ends on null (an undefined figure), or whose path is missing while another slot resolves it, is left out of the ranking and gets a PULSE_SWEEP_RANK_PATH warning on its own Response naming the slot (details label, by, reason, segment). A nil slot response is skipped. The call is refused with PULSE_SWEEP_RANK_PATH, the first offending slot in expansion order under details.label and the failing segment under details.segment, when a path ends on a non-number or is ambiguous in any slot, or resolves in no slot.
Ties keep expansion order; Rank is 1-based; Top trims the ranking only.
Types ¶
type Expansion ¶
type Expansion struct {
// Composed is the effective request. Without a sweep it is the input
// pointer itself, untouched. With one it is a shallow clone whose
// Requests are the explicit slots (the caller's pointers, in order)
// followed by one slot per sweep combination in expansion order,
// whose Overlays are the explicit specs followed by each sweep
// slot's substituted `sweep.overlays` in expansion order, and whose
// Sweep is nil — the effective request is sweep-free, so running or
// echoing it never expands twice.
Composed *types.ComposedRequest
// Explicit is the number of explicit (hand-written) slots; the sweep
// slots are Composed.Requests[Explicit:].
Explicit int
// Labels are the sweep slots' labels in expansion order (nil without
// a sweep).
Labels []string
}
Expansion is a ComposedRequest with its sweep expanded: the effective request every Compose surface runs, predicts and label-checks.
func Expand ¶
Expand turns c into its effective request — the ONE expansion every Compose surface consumes (Compose and ComposeParallel in the runtime, PredictCompose, the overlay resolve, the EchoRequest echo), so their slot lists and label namespaces cannot drift.
Order of checks: Validate; then the nested-for_each refusal (an item in the body carrying `for_each` is PULSE_FOR_EACH_INVALID reason nested_in_sweep); then the reference rule (every axis is named by a placeholder in the request body, the AUTHORED label pattern or the overlays — the default label does not count — and every placeholder names an axis); then preflight, when non-nil, with the total slot count (explicit + expanded) BEFORE anything is rendered, so an oversized product is refused without being built; then one substitution per combination through internal/template — `{{axis}}` renders the value's text exactly as written, `{"$var": "axis"}` splices it type-preserving — and a STRICT decode of each body into a Request. Faults are PULSE_SWEEP_INVALID (a preflight error is returned unchanged).
Without a sweep Expand calls preflight with len(c.Requests) and returns c itself. Label collisions are NOT checked here: each collision site checks the effective slot list it is handed, explicit and sweep slots in one namespace.
type FanOut ¶
type FanOut struct {
// Path is the item's request-rooted path, e.g. `windows[0]`.
Path string
// Axes are the item's axes in declared order.
Axes []types.SweepAxis
// Mode is the resolved mode: grid (the default) or zip.
Mode types.SweepMode
// Count is the number of items the for_each expands to.
Count int
}
FanOut describes one for_each item: where it sits, its axes and the number of items it expands to.
func ExpandItems ¶
func ExpandItems(req *types.Request, preflight func(derived int) error) (*types.Request, []FanOut, error)
ExpandItems is the ONE for_each expansion every surface runs — Process (and through it every Compose slot), ProcessChain stages, Materialize, and predict — so the item lists they judge and run cannot drift.
Order of checks: every for_each item, in pipeline order (features, attributes, windows) and declared order within a list, is checked structurally (the compose sweep's axis rules) and by the reference rule (every axis named by a placeholder in the item, every placeholder naming an axis); then preflight, when non-nil, with DerivedCount — plain items plus expanded ones — BEFORE anything is rendered, so an oversized product is refused without being built; then each for_each item renders once per combination (grid: first axis slowest; zip: lockstep) through internal/template — `{{axis}}` renders the value's text exactly as written, `{"$var": "axis"}` splices it type-preserving — and each rendered item is decoded STRICTLY into its slot type. Faults are PULSE_FOR_EACH_INVALID (a preflight error is returned unchanged).
Without a for_each ExpandItems calls preflight and returns req itself and no fan-outs. With one it returns a shallow clone whose Features, Attributes and Windows are the expanded lists — each for_each item replaced in place by its rendered items, plain items kept (the caller's pointers) — and one FanOut per for_each item. req is never modified.