Skip to content

delta mode: emit a versioned change_risk block (change-level risk score) #202

Description

@stephenc222

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:

  1. Floating-point noise: functions are reported modified when LRS differs only by ~1e-15 (e.g. 3.9 → 3.9000000000000004).
  2. modified appears on functions the change likely did not edit (7 modified on a 1-line PR, mitchellh/cli@25f4c7c).
  3. 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

  • change_risk present in --mode delta --format json; absent from other modes.
  • Golden tests: no-op change → value: 0, changed_functions: 0 (noise suppressed); single-function edit; added function; band transition into critical.
  • Byte-identical output across runs/machines (relative IDs, deterministic ordering).
  • schema_version bumped (minor); older consumers unaffected.
  • Docs: docs/hotspots-cli-contract.md updated with the definitions above.

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions