Rescore via static reporting-job nodes (alt. to #2414)#2417
Open
Rescore via static reporting-job nodes (alt. to #2414)#2417
Conversation
Introduces a rescore mode in the graph executor: GraphBuilder.WithRescore wires reporting jobs whose QrId is in the supplied scores map as StaticReportingJobNodes holding the pre-computed score, while aggregate reporting jobs still roll up their children via the normal ReportingJobNode. No queries run, no executionManager, no datapoint finisher — the initial priority-queue pass propagates static scores through the graph and the executor exits. This gives the server a way to recompute aggregate policy, control, framework, and asset scores from pre-computed leaf scores (e.g. after an exception or scoring-system change) without re-executing any MQL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
New rescore mode enables re-aggregating policy scores without re-running queries
Additional findings (file/line not in diff):
- 🔵
policy/executor/internal/graph.go:104— Every node (including static leaf nodes) is pushed into the priority queue withmaxPriority. In rescore mode all static nodes will emit their score on the firstrecalculate(), and all aggregate nodes will alsorecalculate()— but aggregate nodes will produceniluntil their children have delivered scores viaconsume. Because the priority queue usesmaxPriorityfor every initial push, processing order within the first drain depends on insertion order from the map iterator (non-deterministic).
This works correctly today because recalculate returning nil just means no propagation, and the child's recalculate will re-push the parent. However, this means aggregate nodes may be popped and recalculated multiple times: once during the initial sweep (returning nil), then again after each child delivers a score.
Consider seeding only leaf (static) nodes into the initial queue in rescore mode, or using the already-computed priorityMap values instead of maxPriority for the initial push, to avoid redundant recalculations on aggregate nodes.
Contributor
…consume In rescore mode, only seed StaticReportingJobNode entries into the initial priority queue; aggregate nodes will be queued via consume + cascade once a static node emits, eliminating redundant nil-recalculates during the initial sweep. Surface mis-wiring on the static node: consume() now emits a debug log instead of silently dropping the envelope, so an inadvertently-added inbound edge is visible during development. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
preslavgerchev
approved these changes
Apr 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Alternative approach to #2414 for score rollup via the graph executor. Same end goal — let callers (typically the server) recompute aggregate policy/control/framework/asset scores from pre-computed leaf scores without re-running MQL — but without the
Producerabstraction.The core idea: at build time, when we know which reporting jobs already have scores, swap their graph node for a
StaticReportingJobNodethat holds the fixed score. Aggregate reporting jobs (POLICY, CONTROL, FRAMEWORK) remain normalReportingJobNodes and roll up their children through the existingScoringSystemlogic. No external envelope-addressing producer, nopreseed, no node-state hijacking.GraphBuilder.WithRescore(scores map[string]*policy.Score)flips the builder into rescore mode. In rescore mode:QrId(after the"root" → assetMrnrename) is a key inscoresbecomes aStaticReportingJobNodeholding the supplied score; otherwise it's a normal aggregate node.Execute()exits as soon as the initial priority-queue pass propagates static scores through theirNotifyedges and into theScoreCollector.New public entry point
executor.RescoreResolvedPolicy(assetMrn, rp, scores, scoreCollector).builderFromResolvedPolicyswitched to nil-safe proto getters so minimalResolvedPolicytest fixtures work.Scope note
This PR intentionally covers Value rollup only — no
RiskScorerollup. Risk-score rollup can be layered on later as an orthogonal option.Test plan
go test ./policy/executor/...passes, including 8 newTestRescore_*tests.TestRescore_RealResolvedPolicyexercises a real server-produced resolved policy (159 reporting jobs, 146 leaf scores) and verifies every leaf Value round-trips and every rolled-up score has a valid type.go test ./policy/...passes.go build ./...clean.cnspec scan localstill runs normally (the rescore path is opt-in viaWithRescore; the defaultExecuteResolvedPolicypath is unchanged apart from the nil-safe getters inbuilderFromResolvedPolicy).🤖 Generated with Claude Code