@@ -55,6 +55,7 @@ const (
5555
5656// Scorer types selectable from configuration.
5757const (
58+ scorerTypeEvidence = "evidence"
5859 scorerTypeHeuristic = "heuristic"
5960 scorerTypeComposite = "composite"
6061)
@@ -69,10 +70,7 @@ const (
6970// Ways a composite scorer combines its components.
7071const combineAvg = "avg"
7172
72- // Predictor types selectable from configuration.
73- const predictorTypeEvidence = "evidence"
74-
75- // Evidence an evidence predictor prices, as named in configuration. The set is
73+ // Evidence an evidence scorer prices, as named in configuration. The set is
7674// closed: a factor under any other name would be applied to nothing and never
7775// noticed.
7876const (
@@ -128,7 +126,6 @@ type namedQueueProfileConfig struct {
128126 Analyzer * analyzerConfig `yaml:"analyzer"`
129127 Scorer * scorerConfig `yaml:"scorer"`
130128 Speculator * speculatorConfig `yaml:"speculator"`
131- Predictor * predictorConfig `yaml:"predictor"`
132129}
133130
134131// queueProfileConfig is the full set of extensions a queue resolves to.
@@ -138,7 +135,6 @@ type queueProfileConfig struct {
138135 Analyzer analyzerConfig `yaml:"analyzer"`
139136 Scorer scorerConfig `yaml:"scorer"`
140137 Speculator speculatorConfig `yaml:"speculator"`
141- Predictor predictorConfig `yaml:"predictor"`
142138}
143139
144140// changeProviderConfig selects how change metadata is fetched. The github and
@@ -228,11 +224,19 @@ type analyzerConfig struct {
228224 FailAlways bool `yaml:"failAlways"`
229225}
230226
231- // scorerConfig selects how a queue ranks candidate speculation paths. There is
232- // no scoring stage: the scorer feeds the queue's speculator, which is composed
233- // from it rather than configured separately .
227+ // scorerConfig selects how a queue ranks candidate speculation paths. The
228+ // ranking scorer is evidence wrapping a nested content base. Heuristic and
229+ // composite belong on base (and on composite components), not at the top level .
234230type scorerConfig struct {
235231 Type string `yaml:"type"`
232+ // Factors revise the base price, one per piece of evidence (evidence only).
233+ // An omitted key keeps the inherited value, or 1 if neither defaults nor
234+ // the queue named it.
235+ Factors map [string ]float64 `yaml:"factors"`
236+ // Base is the content scorer evidence revises (evidence only). An omitted
237+ // base on defaults is the default heuristic; a present base on a queue
238+ // replaces the default base wholesale.
239+ Base * scorerConfig `yaml:"base"`
236240 // Buckets map a batch's total lines changed onto a score (heuristic only).
237241 Buckets []bucketConfig `yaml:"buckets"`
238242 // Components are the scorers a composite combines, keyed by name.
@@ -250,7 +254,7 @@ type bucketConfig struct {
250254}
251255
252256// speculatorConfig tunes how much CI a queue's speculation may occupy. It has no
253- // `type`: there is one speculator, composed from the queue's predictor , and what
257+ // `type`: there is one speculator, composed from the queue's scorer , and what
254258// varies between queues is what it is allowed to spend.
255259type speculatorConfig struct {
256260 // BuildBudget caps how many builds this queue may have occupying CI at once,
@@ -259,18 +263,6 @@ type speculatorConfig struct {
259263 BuildBudget int `yaml:"buildBudget"`
260264}
261265
262- // predictorConfig tunes how a queue turns its scorer's price into the
263- // probability the generator ranks on. The scorer being revised is the queue's
264- // own, so it is not named again here.
265- type predictorConfig struct {
266- Type string `yaml:"type"`
267- // Factors revise the scorer's price, one per piece of evidence and keyed by
268- // evidence name. An omitted key keeps the inherited value, or 1 if neither
269- // defaults nor the queue named it. An omitted predictor block inherits the
270- // whole default, so every factor stays 1 until someone sets one.
271- Factors map [string ]float64 `yaml:"factors"`
272- }
273-
274266// loadProfilesConfig reads and validates the profiles configuration at path.
275267func loadProfilesConfig (path string ) (profilesConfig , error ) {
276268 data , err := os .ReadFile (path )
@@ -323,7 +315,7 @@ func (c *profilesConfig) normalizeAndValidate() error {
323315 }
324316 }
325317 if q .Scorer != nil {
326- if err := q .Scorer .normalizeAndValidate (where ); err != nil {
318+ if err := q .Scorer .normalizeOverlay (where ); err != nil {
327319 return err
328320 }
329321 }
@@ -332,11 +324,6 @@ func (c *profilesConfig) normalizeAndValidate() error {
332324 return err
333325 }
334326 }
335- if q .Predictor != nil {
336- if err := q .Predictor .normalizeAndValidate (where ); err != nil {
337- return err
338- }
339- }
340327 }
341328 return c .validateGitRepoPaths ()
342329}
@@ -390,33 +377,33 @@ func (c profilesConfig) resolve(q namedQueueProfileConfig) queueProfileConfig {
390377 profile .Analyzer = * q .Analyzer
391378 }
392379 if q .Scorer != nil {
393- profile .Scorer = * q .Scorer
380+ profile .Scorer = overlayScorer ( profile . Scorer , * q .Scorer )
394381 }
395382 if q .Speculator != nil {
396383 profile .Speculator = * q .Speculator
397384 }
398- if q .Predictor != nil {
399- profile .Predictor = overlayPredictor (profile .Predictor , * q .Predictor )
400- }
401385 return profile
402386}
403387
404- // overlayPredictor keeps default factors the queue did not name. A present
405- // predictor block is otherwise a normal extension override: type replaces when
406- // set, and named factor keys win .
407- func overlayPredictor (base , override predictorConfig ) predictorConfig {
388+ // overlayScorer keeps default factors the queue did not name. A present base
389+ // replaces the default base wholesale. Type stays evidence unless the override
390+ // names one, which must still be evidence .
391+ func overlayScorer (base , override scorerConfig ) scorerConfig {
408392 if override .Type != "" {
409393 base .Type = override .Type
410394 }
411- if len (override .Factors ) == 0 {
412- return base
395+ if len (override .Factors ) > 0 {
396+ merged := maps .Clone (base .Factors )
397+ if merged == nil {
398+ merged = make (map [string ]float64 , len (override .Factors ))
399+ }
400+ maps .Copy (merged , override .Factors )
401+ base .Factors = merged
413402 }
414- merged := maps . Clone ( base . Factors )
415- if merged == nil {
416- merged = make ( map [ string ] float64 , len ( override . Factors ))
403+ if override . Base != nil {
404+ copied := * override . Base
405+ base . Base = & copied
417406 }
418- maps .Copy (merged , override .Factors )
419- base .Factors = merged
420407 return base
421408}
422409
@@ -436,7 +423,7 @@ func (p *queueProfileConfig) normalizeAndValidate(where string) error {
436423 if err := p .Speculator .normalizeAndValidate (where ); err != nil {
437424 return err
438425 }
439- return p . Predictor . normalizeAndValidate ( where )
426+ return nil
440427}
441428
442429func (c * changeProviderConfig ) normalizeAndValidate (where string ) error {
@@ -576,10 +563,56 @@ func (a *analyzerConfig) normalizeAndValidate(where string) error {
576563 }
577564}
578565
579- // normalizeAndValidate applies defaults and rejects a scorer that could not be
580- // built. An empty block is a flat heuristic: every batch scores the same, which
581- // is the neutral choice for a queue with no opinion about ordering .
566+ // normalizeAndValidate applies defaults and rejects a ranking scorer that
567+ // could not be built. An empty block is evidence wrapping the default
568+ // heuristic, with every factor neutral .
582569func (s * scorerConfig ) normalizeAndValidate (where string ) error {
570+ return s .normalizeRanking (where , true )
571+ }
572+
573+ // normalizeOverlay validates a queue's scorer override without inventing a
574+ // base: omitted base means inherit the default base.
575+ func (s * scorerConfig ) normalizeOverlay (where string ) error {
576+ return s .normalizeRanking (where , false )
577+ }
578+
579+ func (s * scorerConfig ) normalizeRanking (where string , fillBase bool ) error {
580+ if s .Type == "" {
581+ s .Type = scorerTypeEvidence
582+ }
583+ if s .Type != scorerTypeEvidence {
584+ return fmt .Errorf ("%s: scorer type %q belongs under base, not at the ranking layer" , where , s .Type )
585+ }
586+ if err := validateFactors (where , s .Factors ); err != nil {
587+ return err
588+ }
589+ if s .Base != nil {
590+ return s .Base .normalizeContent (where + " base" )
591+ }
592+ if fillBase {
593+ s .Base = & scorerConfig {}
594+ return s .Base .normalizeContent (where + " base" )
595+ }
596+ return nil
597+ }
598+
599+ func validateFactors (where string , factors map [string ]float64 ) error {
600+ for name , factor := range factors {
601+ switch name {
602+ case factorPathPassed , factorPathFailed , factorMerging , factorCancelling :
603+ default :
604+ return fmt .Errorf ("%s: unknown scorer factor %q" , where , name )
605+ }
606+ // Zero would permanently pin matching batches to 0; negatives cannot
607+ // represent either direction in the factor contract.
608+ if ! (factor > 0 ) || math .IsInf (factor , 0 ) {
609+ return fmt .Errorf ("%s: scorer factor %q is %v, must be finite and positive" , where , name , factor )
610+ }
611+ }
612+ return nil
613+ }
614+
615+ func (s * scorerConfig ) normalizeContent (where string ) error {
583616 if s .Type == "" {
584617 s .Type = scorerTypeHeuristic
585618 }
@@ -601,7 +634,7 @@ func (s *scorerConfig) normalizeAndValidate(where string) error {
601634 return fmt .Errorf ("%s: composite scorer needs at least one component" , where )
602635 }
603636 for name , component := range s .Components {
604- if err := component .normalizeAndValidate (fmt .Sprintf ("%s component %q" , where , name )); err != nil {
637+ if err := component .normalizeContent (fmt .Sprintf ("%s component %q" , where , name )); err != nil {
605638 return err
606639 }
607640 s .Components [name ] = component
@@ -618,31 +651,6 @@ func (s *scorerConfig) normalizeAndValidate(where string) error {
618651 return nil
619652}
620653
621- // normalizeAndValidate applies defaults and rejects a predictor that could not
622- // be built. An empty block is an evidence predictor with every factor neutral,
623- // which prices a batch at exactly its scorer's price.
624- func (p * predictorConfig ) normalizeAndValidate (where string ) error {
625- if p .Type == "" {
626- p .Type = predictorTypeEvidence
627- }
628- if p .Type != predictorTypeEvidence {
629- return fmt .Errorf ("%s: unknown predictor type %q" , where , p .Type )
630- }
631- for name , factor := range p .Factors {
632- switch name {
633- case factorPathPassed , factorPathFailed , factorMerging , factorCancelling :
634- default :
635- return fmt .Errorf ("%s: unknown predictor factor %q" , where , name )
636- }
637- // Zero would permanently pin matching batches to 0; negatives cannot
638- // represent either direction in the factor contract.
639- if ! (factor > 0 ) || math .IsInf (factor , 0 ) {
640- return fmt .Errorf ("%s: predictor factor %q is %v, must be finite and positive" , where , name , factor )
641- }
642- }
643- return nil
644- }
645-
646654func (s * speculatorConfig ) normalizeAndValidate (where string ) error {
647655 // A negative budget is rejected rather than clamped: sticky would compute no
648656 // free slots from it, so the queue would batch and then never build anything,
0 commit comments