Add evidence context policy preparation
This commit is contained in:
@@ -28,6 +28,7 @@ var _ contracts.OutputEncoder = (*Encoder)(nil)
|
||||
|
||||
type Options struct {
|
||||
IncludeChunkMap bool
|
||||
EvidenceContext pipeline.EvidenceContextPolicy
|
||||
}
|
||||
|
||||
type Encoder struct {
|
||||
@@ -39,6 +40,7 @@ func New() *Encoder {
|
||||
}
|
||||
|
||||
func NewWithOptions(options Options) *Encoder {
|
||||
options.EvidenceContext.LaneIDs = append([]string(nil), options.EvidenceContext.LaneIDs...)
|
||||
return &Encoder{options: options}
|
||||
}
|
||||
|
||||
@@ -46,6 +48,15 @@ func (e *Encoder) Key() string {
|
||||
return Key
|
||||
}
|
||||
|
||||
func (e *Encoder) EvidenceContextPolicy() pipeline.EvidenceContextPolicy {
|
||||
if e == nil {
|
||||
return pipeline.EvidenceContextPolicy{}
|
||||
}
|
||||
policy := e.options.EvidenceContext
|
||||
policy.LaneIDs = append([]string(nil), policy.LaneIDs...)
|
||||
return policy
|
||||
}
|
||||
|
||||
func (e *Encoder) Encode(ctx context.Context, req contracts.OutputRequest) (contracts.OutputResult, error) {
|
||||
if e == nil {
|
||||
return contracts.OutputResult{}, encoderErrorf("encoder must not be nil")
|
||||
@@ -74,7 +85,7 @@ func ModuleSpec() pipeline.ModuleSpec {
|
||||
}
|
||||
|
||||
func Register(registry *pipeline.OutputEncoderRegistry) error {
|
||||
return registry.RegisterBuilderWithSpec(ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.OutputEncoder, error) {
|
||||
return registry.RegisterBuilderWithProfileValidation(ModuleSpec(), validateOptions, validateProfileOptions, func(request pipeline.BuildRequest) (contracts.OutputEncoder, error) {
|
||||
options, err := DecodeOptions(request.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -89,17 +100,126 @@ func validateOptions(options map[string]any) error {
|
||||
}
|
||||
|
||||
func DecodeOptions(options map[string]any) (Options, error) {
|
||||
if err := pipeline.RejectUnknownOptions(options, "include_chunk_map"); err != nil {
|
||||
if err := pipeline.RejectUnknownOptions(options, "include_chunk_map", "evidence_context"); err != nil {
|
||||
return Options{}, encoderErrorf("%w", err)
|
||||
}
|
||||
decoded := Options{}
|
||||
if value, ok := options["include_chunk_map"]; ok {
|
||||
enabled, ok := value.(bool)
|
||||
if !ok {
|
||||
return Options{}, encoderErrorf("option %q must be a boolean", "include_chunk_map")
|
||||
}
|
||||
return Options{IncludeChunkMap: enabled}, nil
|
||||
decoded.IncludeChunkMap = enabled
|
||||
}
|
||||
return Options{}, nil
|
||||
if value, ok := options["evidence_context"]; ok {
|
||||
policy, err := decodeEvidenceContextPolicy(value)
|
||||
if err != nil {
|
||||
return Options{}, err
|
||||
}
|
||||
decoded.EvidenceContext = policy
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
func validateProfileOptions(context pipeline.OutputProfileOptionContext, options map[string]any) error {
|
||||
decoded, err := DecodeOptions(options)
|
||||
if err != nil || !decoded.EvidenceContext.Enabled {
|
||||
return err
|
||||
}
|
||||
configured := make(map[string]struct{}, len(context.LaneIDs))
|
||||
for _, laneID := range context.LaneIDs {
|
||||
configured[laneID] = struct{}{}
|
||||
}
|
||||
for _, laneID := range decoded.EvidenceContext.LaneIDs {
|
||||
if _, ok := configured[laneID]; !ok {
|
||||
return encoderErrorf("evidence_context lane %q is not configured", laneID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeEvidenceContextPolicy(value any) (pipeline.EvidenceContextPolicy, error) {
|
||||
object, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return pipeline.EvidenceContextPolicy{}, encoderErrorf("option %q must be an object", "evidence_context")
|
||||
}
|
||||
if err := pipeline.RejectUnknownOptions(object, "enabled", "lanes", "window_units"); err != nil {
|
||||
return pipeline.EvidenceContextPolicy{}, encoderErrorf("evidence_context: %w", err)
|
||||
}
|
||||
enabledValue, ok := object["enabled"]
|
||||
if !ok {
|
||||
return pipeline.EvidenceContextPolicy{}, encoderErrorf("evidence_context option %q is required", "enabled")
|
||||
}
|
||||
enabled, ok := enabledValue.(bool)
|
||||
if !ok {
|
||||
return pipeline.EvidenceContextPolicy{}, encoderErrorf("evidence_context option %q must be a boolean", "enabled")
|
||||
}
|
||||
if !enabled {
|
||||
if _, ok := object["lanes"]; ok {
|
||||
return pipeline.EvidenceContextPolicy{}, encoderErrorf("evidence_context option %q is not allowed when disabled", "lanes")
|
||||
}
|
||||
if _, ok := object["window_units"]; ok {
|
||||
return pipeline.EvidenceContextPolicy{}, encoderErrorf("evidence_context option %q is not allowed when disabled", "window_units")
|
||||
}
|
||||
return pipeline.EvidenceContextPolicy{}, nil
|
||||
}
|
||||
rawLanes, ok := object["lanes"]
|
||||
if !ok {
|
||||
return pipeline.EvidenceContextPolicy{}, encoderErrorf("evidence_context option %q is required when enabled", "lanes")
|
||||
}
|
||||
lanes, err := decodeEvidenceLaneIDs(rawLanes)
|
||||
if err != nil {
|
||||
return pipeline.EvidenceContextPolicy{}, err
|
||||
}
|
||||
windowUnits := 3
|
||||
if rawWindow, ok := object["window_units"]; ok {
|
||||
value, ok := rawWindow.(int)
|
||||
if !ok {
|
||||
return pipeline.EvidenceContextPolicy{}, encoderErrorf("evidence_context option %q must be an integer", "window_units")
|
||||
}
|
||||
if value < 0 {
|
||||
return pipeline.EvidenceContextPolicy{}, encoderErrorf("evidence_context option %q must not be negative", "window_units")
|
||||
}
|
||||
windowUnits = value
|
||||
}
|
||||
return pipeline.EvidenceContextPolicy{Enabled: true, WindowUnits: windowUnits, LaneIDs: lanes}, nil
|
||||
}
|
||||
|
||||
func decodeEvidenceLaneIDs(value any) ([]string, error) {
|
||||
var raw []any
|
||||
switch typed := value.(type) {
|
||||
case []any:
|
||||
raw = typed
|
||||
case []string:
|
||||
raw = make([]any, len(typed))
|
||||
for i := range typed {
|
||||
raw[i] = typed[i]
|
||||
}
|
||||
default:
|
||||
return nil, encoderErrorf("evidence_context option %q must be an array", "lanes")
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return nil, encoderErrorf("evidence_context option %q must not be empty", "lanes")
|
||||
}
|
||||
seen := make(map[string]struct{}, len(raw))
|
||||
lanes := make([]string, 0, len(raw))
|
||||
for _, value := range raw {
|
||||
lane, ok := value.(string)
|
||||
if !ok {
|
||||
return nil, encoderErrorf("evidence_context lane values must be strings")
|
||||
}
|
||||
lane = strings.TrimSpace(lane)
|
||||
if lane == "" {
|
||||
return nil, encoderErrorf("evidence_context lane values must not be empty")
|
||||
}
|
||||
if _, ok := seen[lane]; ok {
|
||||
return nil, encoderErrorf("evidence_context lane %q is duplicated", lane)
|
||||
}
|
||||
seen[lane] = struct{}{}
|
||||
lanes = append(lanes, lane)
|
||||
}
|
||||
sort.Strings(lanes)
|
||||
return lanes, nil
|
||||
}
|
||||
|
||||
type indexFile struct {
|
||||
|
||||
@@ -68,13 +68,62 @@ func TestDecodeOptions(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeOptions() error = %v, want nil", err)
|
||||
}
|
||||
if got != test.want {
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("DecodeOptions() = %#v, want %#v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeEvidenceContextOptions(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
options map[string]any
|
||||
want pipeline.EvidenceContextPolicy
|
||||
wantErr string
|
||||
}{
|
||||
{name: "disabled", options: map[string]any{"evidence_context": map[string]any{"enabled": false}}},
|
||||
{name: "enabled default window", options: map[string]any{"evidence_context": map[string]any{"enabled": true, "lanes": []any{"npcs"}}}, want: pipeline.EvidenceContextPolicy{Enabled: true, WindowUnits: 3, LaneIDs: []string{"npcs"}}},
|
||||
{name: "explicit zero window and normalized lanes", options: map[string]any{"evidence_context": map[string]any{"enabled": true, "lanes": []any{" spells ", "npcs"}, "window_units": 0}}, want: pipeline.EvidenceContextPolicy{Enabled: true, WindowUnits: 0, LaneIDs: []string{"npcs", "spells"}}},
|
||||
{name: "duplicate lanes", options: map[string]any{"evidence_context": map[string]any{"enabled": true, "lanes": []any{"npcs", " npcs "}}}, wantErr: "duplicated"},
|
||||
{name: "unknown nested option", options: map[string]any{"evidence_context": map[string]any{"enabled": false, "extra": true}}, wantErr: "unknown option"},
|
||||
{name: "disabled nested fields", options: map[string]any{"evidence_context": map[string]any{"enabled": false, "lanes": []any{"npcs"}}}, wantErr: "not allowed"},
|
||||
{name: "invalid object", options: map[string]any{"evidence_context": true}, wantErr: "must be an object"},
|
||||
{name: "invalid lane type", options: map[string]any{"evidence_context": map[string]any{"enabled": true, "lanes": "npcs"}}, wantErr: "must be an array"},
|
||||
{name: "invalid window type", options: map[string]any{"evidence_context": map[string]any{"enabled": true, "lanes": []any{"npcs"}, "window_units": "3"}}, wantErr: "must be an integer"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := DecodeOptions(test.options)
|
||||
if test.wantErr != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
|
||||
t.Fatalf("DecodeOptions() error = %v, want %q", err, test.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeOptions() error = %v, want nil", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got.EvidenceContext, test.want) {
|
||||
t.Fatalf("EvidenceContext = %#v, want %#v", got.EvidenceContext, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileValidationRejectsUnknownEvidenceLaneAndCopiesInputs(t *testing.T) {
|
||||
registry := pipeline.NewOutputEncoderRegistry()
|
||||
if err := Register(registry); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
options := map[string]any{"evidence_context": map[string]any{"enabled": true, "lanes": []any{"npcs"}}}
|
||||
if err := registry.ValidateProfileOptions(Key, pipeline.OutputProfileOptionContext{LaneIDs: []string{"npcs", "spells"}}, options); err != nil {
|
||||
t.Fatalf("ValidateProfileOptions() error = %v, want nil", err)
|
||||
}
|
||||
if err := registry.ValidateProfileOptions(Key, pipeline.OutputProfileOptionContext{LaneIDs: []string{"spells"}}, options); err == nil || !strings.Contains(err.Error(), "not configured") {
|
||||
t.Fatalf("ValidateProfileOptions() error = %v, want unknown lane failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeReturnsLogicalFilesForNormalizedOutputs(t *testing.T) {
|
||||
req := contracts.OutputRequest{
|
||||
Manifest: artifacts.RunManifest{RunID: "run-1", PipelineID: "pipeline-1"},
|
||||
|
||||
Reference in New Issue
Block a user