Problem
hotspots analyze --mode delta returns per-function deltas and per-file aggregates, but no change-level (commit- or PR-level) score.
Consumers (hosted API, CI comments, an internal PR-risk vs. defect study) each re-derive one
from deltas[], and three pilot findings make naive re-derivation unreliable:
- Floating-point noise: functions are reported
modified when LRS differs only by ~1e-15 (e.g. 3.9 → 3.9000000000000004).
modified appears on functions the change likely did not edit (7 modified on a 1-line PR, mitchellh/cli@25f4c7c).
function_id embeds the absolute clone path (/private/tmp/pilot-cli/./cli.go::Run), so IDs don't join across runs or machines.
Proposal
Add an additive, optional top-level change_risk object to the delta JSON (bump schema_version minor; no existing
field changes):
"change_risk": {
"scope": { "kind": "range", "base": "<parent sha>", "head": "<commit sha>" },
"score": { "kind": "sum_positive_delta_lrs", "version": "1.0.0", "value": 8.4 },
"components": {
"max_delta_lrs": 3.1,
"sum_positive_delta_lrs": 8.4,
"new_critical_functions": 1,
"changed_functions": 6
},
"inputs": { "functions_scored": 6, "noise_epsilon": 1e-9 },
"tool_version": "1.x.y"
}
change_risk is deliberately not PR-specific: delta mode compares any two commits, so the CLI fills scope.kind
with commit or range. Callers that know more (e.g. a hosted API) may set scope.kind to pull_request and add
the PR reference. scope.kind is an open enum (commit, range, pull_request, release). The PR-scoped
instance is the concrete case this was motivated by (informally, a "PR score").
Definitions (frozen per version)
- Scored function set: functions with
status in {added, modified, removed} and |Δlrs| > noise_epsilon or a band transition, and whose line range overlaps a changed hunk (when git diff is available; otherwise skip the overlap filter and say so in inputs).
sum_positive_delta_lrs: sum over the scored set of max(0, after.lrs - before.lrs); an added function contributes its full after.lrs.
max_delta_lrs: max of the same per-function values.
new_critical_functions: scored functions whose after.band is critical and before was absent or not critical.
changed_functions: size of the scored set.
noise_epsilon: 1e-9 default, echoed in the output.
Related fixes (same PR or split)
- Emit
function_id (and file paths in aggregates) relative to the repo root.
- Include
tool_version in the delta output (snapshots already have it).
- Suppress
status: modified when metrics and LRS are unchanged within epsilon (report as unchanged).
Why in the CLI
The definition of the score should be versioned with the analysis that produces its inputs: one source of truth
for CI, the hosted API and research. Downstream may still compute their own from deltas[]; this makes the
default well-defined and reproducible.
Non-goals
- No claim that the score predicts defects. It is unvalidated (under evaluation in an internal study);
change_risk carries no tier, percentile or validation claim.
- No PR number/merge SHA (the CLI doesn't know them; the caller adds those).
Acceptance criteria
Notes
- The study spec fixes
sum_positive_delta_lrs as its primary predictor before any scoring; changing definitions later requires a new version.
- CLI 1.26.0 was used in the pilot; confirm behaviour on the latest release before implementing.
Framing (added 2026-09-20)
change_risk is a derived summary of deltas[], not new source data: a PR is a grouping of per-function records.
deltas[] stays the source of truth and must remain complete in the output. Any future score definition must be
recomputable from deltas[] alone. Downstream stores can keep per-function rows keyed with PR grouping fields
(pr_number, merge_sha) added by the caller, with change_risk as an optional derived block.
Weighting and aggregation (added 2026-09-20)
change_risk.score declares how it was built so alternatives can be added without breaking consumers:
"change_risk": {
"scope": { "kind": "range", "base": "…", "head": "…" },
"score": {
"kind": "weighted_aggregate", "version": "1.0.0", "value": 8.4,
"term": "positive_delta_lrs",
"weights": { "scheme": "uniform" },
"aggregation": { "operator": "sum" }
},
"components": { "...": "..." }
}
term: per-function quantity aggregated (default positive_delta_lrs).
weights.scheme: uniform (default), and later e.g. fan_in, loc, band; scheme params echoed in the output.
aggregation.operator: sum (default), max, top_k_mean (with k), lp_norm (with p).
- Default remains uniform + sum. Additional schemes/operators are additive;
deltas[] stays complete so any variant is recomputable from it.
Problem
hotspots analyze --mode deltareturns per-function deltas and per-fileaggregates, but no change-level (commit- or PR-level) score.Consumers (hosted API, CI comments, an internal PR-risk vs. defect study) each re-derive one
from
deltas[], and three pilot findings make naive re-derivation unreliable:modifiedwhen LRS differs only by ~1e-15 (e.g.3.9 → 3.9000000000000004).modifiedappears on functions the change likely did not edit (7modifiedon a 1-line PR,mitchellh/cli@25f4c7c).function_idembeds the absolute clone path (/private/tmp/pilot-cli/./cli.go::Run), so IDs don't join across runs or machines.Proposal
Add an additive, optional top-level
change_riskobject to the delta JSON (bumpschema_versionminor; no existingfield changes):
change_riskis deliberately not PR-specific: delta mode compares any two commits, so the CLI fillsscope.kindwith
commitorrange. Callers that know more (e.g. a hosted API) may setscope.kindtopull_requestand addthe PR reference.
scope.kindis an open enum (commit,range,pull_request,release). The PR-scopedinstance is the concrete case this was motivated by (informally, a "PR score").
Definitions (frozen per
version)statusin {added,modified,removed} and|Δlrs| > noise_epsilonor a band transition, and whose line range overlaps a changed hunk (when git diff is available; otherwise skip the overlap filter and say so ininputs).sum_positive_delta_lrs: sum over the scored set ofmax(0, after.lrs - before.lrs); anaddedfunction contributes its fullafter.lrs.max_delta_lrs: max of the same per-function values.new_critical_functions: scored functions whoseafter.bandis critical andbeforewas absent or not critical.changed_functions: size of the scored set.noise_epsilon: 1e-9 default, echoed in the output.Related fixes (same PR or split)
function_id(and file paths inaggregates) relative to the repo root.tool_versionin the delta output (snapshots already have it).status: modifiedwhen metrics and LRS are unchanged within epsilon (report asunchanged).Why in the CLI
The definition of the score should be versioned with the analysis that produces its inputs: one source of truth
for CI, the hosted API and research. Downstream may still compute their own from
deltas[]; this makes thedefault well-defined and reproducible.
Non-goals
change_riskcarries no tier, percentile or validation claim.Acceptance criteria
change_riskpresent in--mode delta --format json; absent from other modes.value: 0,changed_functions: 0(noise suppressed); single-function edit; added function; band transition into critical.schema_versionbumped (minor); older consumers unaffected.docs/hotspots-cli-contract.mdupdated with the definitions above.Notes
sum_positive_delta_lrsas its primary predictor before any scoring; changing definitions later requires a newversion.Framing (added 2026-09-20)
change_riskis a derived summary ofdeltas[], not new source data: a PR is a grouping of per-function records.deltas[]stays the source of truth and must remain complete in the output. Any future score definition must berecomputable from
deltas[]alone. Downstream stores can keep per-function rows keyed with PR grouping fields(
pr_number,merge_sha) added by the caller, withchange_riskas an optional derived block.Weighting and aggregation (added 2026-09-20)
change_risk.scoredeclares how it was built so alternatives can be added without breaking consumers:term: per-function quantity aggregated (defaultpositive_delta_lrs).weights.scheme:uniform(default), and later e.g.fan_in,loc,band; scheme params echoed in the output.aggregation.operator:sum(default),max,top_k_mean(withk),lp_norm(withp).deltas[]stays complete so any variant is recomputable from it.