Skip to content

feat(AF-653): expose approval prediction on the read side - #705

Merged
babltiga merged 2 commits into
mainfrom
feature/AF-653-approval-prediction-read-side
Aug 7, 2026
Merged

feat(AF-653): expose approval prediction on the read side#705
babltiga merged 2 commits into
mainfrom
feature/AF-653-approval-prediction-read-side

Conversation

@babltiga

@babltiga babltiga commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Closes #653. Part of epic #645 (approval-outcome prediction).

Everything below the read side already merged (#646#652): the approval_predictions table, the
core.api services, the serving path and the retrain job. Rows were being written and then thrown
away — no field on any response, no realtime consumer. This exposes them.

Backend read side only. The frontend badge/panel is #654; the docs/website sync sweep is #655.

What changed

Query detailGET /queries/{id} gains an approval_prediction block.
core.api.QueryDetailView grows an ApprovalPredictionDetail nested record, populated in
DefaultQueryRequestLookupService and mapped to the wire by QueryDetailResponse. It carries
id, probability, skipped, skipped_reason, failed, created_at — deliberately not
model_id, feature_schema_version, the feature snapshot, or the failure message. skipped_reason
stays the machine token (DISABLED / MODEL_NOT_SERVING) the serving path wrote; the client
localizes it.

Unlike the AF-624 cost estimate beside it, there is no reciprocal query_requests.approval_prediction_id
FK to short-circuit on, so this is one unconditional lookup on the query_request_id UNIQUE index per
detail fetch. Adding an FK would need a migration this issue does not ask for and would contradict the
schema in docs/03-data-model.md.

Review queueGET /reviews/pending gains a nullable approval_probability per row.
DefaultReviewService.listPendingForReviewer now filters the page to the actionable rows first, then
makes one findByQueryRequestIds call for the whole page and keys it into a Map<UUID, Double>.
A per-row lookup here would be an N+1 on every queue render; DefaultReviewServiceTest pins the
single-call property with verify(..., times(1)) and also covers the no-actionable-rows
short-circuit. Sentinel rows carry no probability and are simply absent from the map.

RealtimeRealtimeEventDispatcher consumes ApprovalPredictionCompletedEvent and pushes
query.prediction_complete. Unlike the submitter-only query.estimate_complete, it fans out to the
review plan's first-stage approvers (the same set review.new_request targets, already excluding the
submitter) plus the submitter, whose detail page renders the same block. The payload is a refetch
trigger, not data: query_id and a probability that is JSON-null on the sentinel rows.

query.prediction_complete is also registered in the frontend WS contract
(WsEventName / WsEventPayloadMap / WS_EVENT_NAMES). Without that entry websocketManager
hard-drops every frame with a console warning, so the event would be inert until #654. That is the
wire contract only — the badge and panel remain #654's scope.

Advisory-only guarantee stays structural: three read surfaces and nothing else. The prediction is
not an input to routing, grant coverage, break-glass, or any decision path.

Docs updated

  • docs/04-api-spec.md — the approval_prediction block in the query-detail
    example plus an explanatory paragraph; approval_probability in the review-queue example plus a
    paragraph; a query.prediction_complete row in the WebSocket events table.
  • docs/05-backend.md — a "Read side (AF-653)" subsection under
    "Approval-outcome prediction", and the event added to the source-events → WS-events registry table.

Not touched, deliberately: docs/03-data-model.md (no schema change), docs/09-deployment.md (no new
env var), README.md and website/ (the feature has no website presence yet — #655 is the dedicated
sync sweep), and e2e/ (no route, selector, form, or user-facing flow changes).

Verification

  • mvn clean verify -Pcoverage5845 tests, 0 failures.
  • mvn test -Dtest='ApplicationModulesTest,ApiPackageDependencyTest' — pass.
  • Frontend lint (0 errors) + typecheck + test:coverage (1333 tests, 91.85% statements /
    83.66% branches) + build — all green.
  • ApprovalPredictionIntegrationTest gains two real-Postgres cases proving the detail view resolves
    the prediction without a reciprocal FK, and that it stays null before scoring.

Review notes

af-verifier, af-reviewer and af-java-reviewer ran concurrently. No Blockers from any of them.
Concerns acted on in 63aa476: the "lowest open stage" doc wording, the missing WS registry row, the
frontend WS union entry, and an unreachable Collectors.toMap merge function. Surviving items:

  • af-java-reviewer — the two QueryDetailView backward-compatible constructors have no
    production callers and silently default the newest component, so a future caller could get
    approvalPrediction == null (indistinguishable from "not scored yet") with no compile error.
    Not acted on: workflow: approval prediction — expose in query detail + review queue + WS event #653 explicitly specifies "Add a backward-compatible constructor delegating with
    null, exactly like the existing cost-estimate constructor chain", and the 24-arg overload it
    mirrors is already on main. Removing them reverts an existing decision beyond this issue's scope.
    Worth a separate look if the chain reaches a fourth arity.

  • af-reviewer — the WS recipient set is the plan's first stage, not the query's currently open
    stage. Exact for the first push, but the late-estimate rescore can publish a second event after a
    stage-1 decision, and that one still targets stage-1 approvers. Not acted on in code: resolving
    the open stage needs the decision list and the routing engine's effective min_approvals, both
    behind workflow-internal beans the realtime module cannot reach — closing it would mean a new
    api surface for a refetch hint. The docs now state the actual behaviour and the reasoning.

  • af-verifier — found a pre-existing dead anchor on main
    (#post-queriesdry-run--response-200 in the AF-624 paragraph, docs/04-api-spec.md). Not from this
    branch; left for a separate docs fix.

One process note for reviewers reading the run logs: an earlier mvn verify reported an
ApiPackageDependencyTest failure ("failed to check any classes"). That was a concurrent-Maven
artifact in my working tree — another invocation rewrote target/classes mid-run. The clean
mvn clean verify -Pcoverage above passes, as does the gate standalone and in a fresh worktree.

Adds the advisory approval-outcome prediction (AF-645) to the three read
surfaces and nothing else, which is what keeps the advisory-only guarantee
structural:

- GET /queries/{id} gains an approval_prediction block, joined in
  DefaultQueryRequestLookupService. There is no reciprocal FK on
  query_requests, so this is one unconditional unique-index lookup per
  detail fetch.
- GET /reviews/pending gains a nullable approval_probability per row,
  resolved for the whole page in a single findByQueryRequestIds call.
- RealtimeEventDispatcher consumes ApprovalPredictionCompletedEvent and
  pushes query.prediction_complete to the eligible reviewers plus the
  submitter.

QueryDetailView and ReviewService.PendingReview grow a component each;
QueryDetailView keeps a backward-compatible 25-arg constructor so the
existing call sites are untouched.

Closes #653
…-side

Review follow-ups:

- docs said the prediction push targets the query's lowest *open* stage;
  eligibleReviewersForLowestStage actually takes the review plan's minimum
  stage unconditionally. Corrected both docs and recorded why resolving the
  open stage is not worth it: it needs the decision list and the routing
  engine's effective min_approvals, both behind workflow-internal beans the
  realtime module cannot reach, for what is only a refetch hint.
- Added the event to the WS registry table in docs/05-backend.md.
- Registered query.prediction_complete in the frontend WS contract
  (WsEventName, WsEventPayloadMap, WS_EVENT_NAMES). Without it
  websocketManager hard-drops every frame with a console warning, so the
  event would be inert until #654. The badge and panel remain #654's scope.
- Dropped the unreachable Collectors.toMap merge function; query_request_id
  is UNIQUE, so a duplicate key should throw rather than resolve silently.
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Frontend Test Results

1 333 tests   1 333 ✅  4m 48s ⏱️
  173 suites      0 💤
    1 files        0 ❌

Results for commit 63aa476.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Frontend Coverage (frontend)

Status Category Percentage Covered / Total
🟢 Lines 93.86% (🎯 90%) 2096 / 2233
🟢 Statements 91.85% (🎯 90%) 2334 / 2541
🟢 Functions 91% (🎯 90%) 637 / 700
🟢 Branches 83.66% (🎯 80%) 1316 / 1573
File CoverageNo changed files found.
Generated in workflow #908 for commit 63aa476 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Backend Test Results

5 845 tests  +19   5 845 ✅ +19   14m 49s ⏱️ - 2m 48s
  713 suites + 1       0 💤 ± 0 
  713 files   + 1       0 ❌ ± 0 

Results for commit 63aa476. ± Comparison against base commit b908d04.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Backend Code Coverage

Overall Project 93.28% 🍏
Files changed 100% 🍏

File Coverage
ApprovalPredictionCompletedEvent.java 100% 🍏
PendingReviewItem.java 100% 🍏
QueryDetailView.java 100% 🍏
ReviewService.java 100% 🍏
DefaultQueryRequestLookupService.java 98.31% 🍏
DefaultReviewService.java 93.71% 🍏
QueryDetailResponse.java 86.56% 🍏
RealtimeEventDispatcher.java 84.63% 🍏

@babltiga
babltiga merged commit 5f225ed into main Aug 7, 2026
52 of 55 checks passed
@babltiga
babltiga deleted the feature/AF-653-approval-prediction-read-side branch August 7, 2026 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

workflow: approval prediction — expose in query detail + review queue + WS event

1 participant