|
| 1 | +# Outcome Scorer |
| 2 | + |
| 3 | +How likely a batch is to reach Succeeded, given the change's content plus what this speculate run has already observed. |
| 4 | + |
| 5 | +See [speculation.md](speculation.md) for batches, paths, heads, and the Speculator. This document specifies the dependency probability the default Generator uses to rank paths. |
| 6 | + |
| 7 | +## The idea |
| 8 | + |
| 9 | +`bestfirst` ranks a path by the probability that every unresolved dependency assumption holds. For each dependency it calls **one** extension: `Scorer.Score(ctx, batch, paths)` — the probability of *succeeds*; it uses the complement for *fails*. |
| 10 | + |
| 11 | +That number has two parts, composed as one scorer: |
| 12 | + |
| 13 | +1. A **base** price for the change from content signals such as its size. Heuristic and composite supply this. They implement the same `Score` and ignore `paths`. |
| 14 | +2. An **evidence** layer that revises the base with facts the speculate run already holds: a path *passed*, a path *failed*, the batch is *landing*, the batch is *cancelling*. |
| 15 | + |
| 16 | +Evidence is the scorer the queue exposes. The base sits under it. There is no sibling Predictor factory. |
| 17 | + |
| 18 | +This is a logit-linear model (a GLM with a logit link) with configured weights: |
| 19 | + |
| 20 | +``` |
| 21 | +logit(p') = logit(p_base) + sum_i w_i x_i |
| 22 | +``` |
| 23 | + |
| 24 | +`p_base` is the heuristic or composite price (the offset). `x_i` are binary features from the path set and batch state. `w_i = log(factor_i)` are YAML weights, not a fitted likelihood. |
| 25 | + |
| 26 | +**Default is a no-op.** Every factor starts at `1` (`w = 0`), so `Score` returns the base price until someone sets a factor. |
| 27 | + |
| 28 | +## What a factor is |
| 29 | + |
| 30 | +A factor is an odds multiplier for one feature. It is not itself a probability: `10` does not mean `0.10`, and `0.3` does not mean the batch is 30% likely to succeed. Equivalently `w = log(factor)` on the logit. |
| 31 | + |
| 32 | +| Value | Weight | Meaning | |
| 33 | +| --- | --- | --- | |
| 34 | +| `1` | `0` | Leave the base price alone (the default if the key is omitted) | |
| 35 | +| greater than `1` | positive | More likely to reach Succeeded | |
| 36 | +| between `0` and `1` (exclusive) | negative | Less likely to reach Succeeded | |
| 37 | + |
| 38 | +Config accepts any finite value greater than `0`; there is no finite upper cap. |
| 39 | + |
| 40 | +The unconfigured base prices every batch at `0.5`. From that price, one factor `f` produces `sigmoid(logit(0.5) + log(f)) = f / (1 + f)`: |
| 41 | + |
| 42 | +| Factor | Price | |
| 43 | +| --- | --- | |
| 44 | +| `1` | 0.50 | |
| 45 | +| `10` | ~0.91 | |
| 46 | +| `12` | ~0.92 | |
| 47 | +| `0.3` | ~0.23 | |
| 48 | +| `0.25` | 0.20 | |
| 49 | + |
| 50 | +A base price of `0.6` with `pathPassed: 10` becomes about `0.94`. `landing: 12` on top of that becomes about `0.995`. |
| 51 | + |
| 52 | +`pathFailed: 0.3` from `0.5` becomes about `0.23`. It applies at most once because the path set has one current entry for the all-*succeeds* path; retry attempts replace that entry rather than adding evidence. `0` is rejected because it would pin matching batches at probability 0. |
| 53 | + |
| 54 | +Odds revision keeps the result in `(0, 1)` and makes the same factor mean the same thing at any base price. Neutral factors return the base unchanged, including `0` or `1`. Adding to the probability provides neither property. A clamp at a small epsilon is a numerical guard around those endpoints, not part of the linear predictor. |
| 55 | + |
| 56 | +This is **not** a fitted GLM: weights are configured, not trained; there is no extra intercept (`p_base` is the offset); features are hand-defined, not learned. |
| 57 | + |
| 58 | +YAML. Evidence is the scorer; the content provider is `base`: |
| 59 | + |
| 60 | +```yaml |
| 61 | +scorer: |
| 62 | + type: evidence |
| 63 | + factors: |
| 64 | + pathPassed: 10 |
| 65 | + pathFailed: 0.3 |
| 66 | + landing: 12 |
| 67 | + cancelling: 0.1 |
| 68 | + base: |
| 69 | + type: heuristic |
| 70 | +``` |
| 71 | +
|
| 72 | +The example values above are guesses, for reading the tables. The shipped default is to omit `factors` (every factor `1`). Omitted `type` is `evidence`. Omitted `base` is the default heuristic. `type: heuristic` and `type: composite` belong on `base` (and on composite `components`), not at the top level. |
| 73 | + |
| 74 | +Profiles may set `factors` under `defaults.scorer` and revise them per queue. An omitted key keeps the inherited value — from defaults, or `1` when neither side named it. A queue `scorer` block overlays named factor keys; a present `base` replaces the default base wholesale. |
| 75 | + |
| 76 | +## Evidence |
| 77 | + |
| 78 | +| YAML key | When it applies | Typical direction | |
| 79 | +| --- | --- | --- | |
| 80 | +| `pathPassed` | Once, if a path that assumes every dependency *succeeds* has *passed* | Up | |
| 81 | +| `pathFailed` | Once, if the path that assumes every dependency *succeeds* has *failed* | Down | |
| 82 | +| `landing` | While the batch is *landing* | Up | |
| 83 | +| `cancelling` | While the batch is *cancelling* | Down | |
| 84 | + |
| 85 | +`bestfirst` already treats a terminal batch as a fact (*Succeeded*, *Failed*, *Cancelled*). The scorer is not asked. *Landing* is not terminal: a land can still fail, so how much it is worth stays a price. |
| 86 | + |
| 87 | +### Only the *succeeds* path counts |
| 88 | + |
| 89 | +The batch being priced is itself a head, so the run may have built it more than once under different assumptions about *its* dependencies. Only one of those builds is evidence. |
| 90 | + |
| 91 | +Take `C` depending on `B`, and `B` depending on `A`. Ranking `C`'s candidates needs the probability that `B` reaches Succeeded, so the Generator calls `Score` with `B` and `B`'s path set. That set can hold two finished builds: |
| 92 | + |
| 93 | +| `B`'s path | What was compiled | |
| 94 | +| --- | --- | |
| 95 | +| `B` with `A` *succeeds* | `B` on top of `A`'s changes | |
| 96 | +| `B` with `A` *fails* | `B` without them | |
| 97 | + |
| 98 | +`B` lands after `A` does, so the first build is a build of the code that will actually land: if it *passed*, `B` is likely to land, and `pathPassed` applies. |
| 99 | + |
| 100 | +The second is a different set of changes. `B` may call something `A` introduces and fail to compile on its own — a *failed* result that says nothing about `B` landing in the normal case. Counting it would push `B` down the ranking over a build it was never going to need, while a green build of the real combination sits in the same set. |
| 101 | + |
| 102 | +So `pathPassed` and `pathFailed` both look only at paths that assume every dependency *succeeds*. Results on any other path are skipped. This is a filter on which results are evidence, not a check on whether an assumption came true — nothing here revisits that. |
| 103 | + |
| 104 | +## Rejected alternatives |
| 105 | + |
| 106 | +Design choices a reader might suggest after the sections above. Each names the alternative, why it fails here, and what this RFC does instead. |
| 107 | + |
| 108 | +### A sibling Predictor factory |
| 109 | + |
| 110 | +Keep `Score(ctx, batch)` for content and add `Predict(ctx, batch, paths)` as a second extension. Ranking only needs one number per unresolved batch; two factories duplicate the per-queue seam. **Instead:** one `Scorer.Score(ctx, batch, paths)`. Evidence is a scorer implementation that wraps a base. |
| 111 | + |
| 112 | +### Put `paths` only on heuristic and composite |
| 113 | + |
| 114 | +Every content backend reads the path set. We tried forwarding path sets through composite: components discarded them. **Instead:** heuristic and composite implement the same `Score` and ignore `paths`. Evidence is the layer that reads them. |
| 115 | + |
| 116 | +### One fitted model for content and evidence |
| 117 | + |
| 118 | +Train a single estimate over diff shape and build outcomes together. Content signals and situation signals change at different rates, need different amounts of data, and would force every queue onto the same content scorer. **Instead:** the base stays per-queue; evidence weights layer on in YAML. `p_base` remains the GLM offset if someone later fits `w`. |
| 119 | + |
| 120 | +### Treat *landing* and *cancelling* as settled in the Generator |
| 121 | + |
| 122 | +Rank a *landing* batch like Succeeded and a *cancelling* batch like Cancelled. We tried and reverted: a land can still fail, so the rank was wrong once outcomes diverged. **Instead:** only terminal states short-circuit in the Generator; *landing* and *cancelling* are scorer features (see [Evidence](#evidence)). |
| 123 | + |
| 124 | +### Let the scorer read the path-set store |
| 125 | + |
| 126 | +`Score` loads path sets from storage on each call — smaller API, fewer parameters. Each call can see a different snapshot mid-run (stale or split-brain relative to the rank the Generator is building). **Instead:** the speculate run reads all path sets as one snapshot and passes the matching set for each dependency. |
0 commit comments