Skip to content

feat(pipeline): Jev classifier front-runner for typed-decision calls - #7

Merged
BeLazy167 merged 7 commits into
mainfrom
feat/jev-classifier
Sep 19, 2026
Merged

BeLazy167 merged 7 commits into
mainfrom
feat/jev-classifier

Conversation

@BeLazy167

@BeLazy167 BeLazy167 commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

Ports the Jev integration (merged on argus-private as #301): TypeSafe Jev (jev-1.13.0, System One eval API) front-runs Argus's typed classification calls, cutting LLM spend on the pipeline's highest-volume classifier decisions. LLM paths stay untouched as the fallback — Jev only decides when its probabilities clear conservative thresholds.

backend/internal/jev — minimal client for POST /v1/systemone: batched questions over one state, pinned model, input-only cost math, jev.call.* telemetry mirroring llm.call.*, 120KB state cap, probability range validation.

Four cascades + one shadow, all fail-safe:

Stage Policy
Addressed judge (auto-resolve on push) p≥0.97 addressed + p≥0.5 visible evidence → resolve; p≤0.05 → keep open; else → LLM judge
Convention-relation classifier (memory write gate) 4-way choice per neighbor, all ≥0.95 → skip LLM; any uncertainty escalates whole batch
Intent verification delivers + per-criterion + per-finding-scope nouls; confident all-clear only
Scoring FP pre-filter fp≥0.97 AND defect≤0.5 → leaves judge prompt, re-enters as score-10 synthetic group
Triage (shadow only) Parallel eval on ≤40-file diffs; agreement logged as jev.shadow.triage, NEVER routed

BYOK credentials: per-installation keys via the encrypted provider_keys store (provider typesafe, repo-level then org-level) — Settings → Providers → TypeSafe Jev card beside Embeddings (masked key, replace/delete, optional custom endpoint). A stored key IS the consent — no flag needed. The env TYPESAFE_API_KEY path still requires jev_classifier opt-in (default OFF). Resolution is lazy — candidate-free pushes pay zero lookups.

Token accounting: Jev spend merges into each stage bucket (AutoResolve, Intent, Scoring, Conventions, Triage); previously-unbilled convention classifier spend is now billed on both legs.

Data egress: sanitized finding text, diff hunks, PR metadata/body, intent, stored conventions → api.typesafe.ai (or stored endpoint), only per the rules above. Documented in docs/self-hosting.md + backend/.env.example.

Test plan

  • go build ./... && go vet ./... && go test ./... — all packages green
  • pnpm lint && pnpm typecheck && pnpm build — green
  • Tests: wire format, cascade thresholds, BYOK precedence (repo > org > env+flag), lazy resolution, custom endpoint, opt-out, bounded shadow join, spend accounting
  • Adversarial-verify gate
  • Shadow-mode data review before promoting triage

Summary by cubic

Adds TypeSafe's Jev classifier as a front-runner for addressed-judge, convention-relation, intent-verification, and scoring false-positive decisions, plus an observe-only triage shadow. Previously every decision went to the LLM; now confident Jev answers skip it, anything uncertain, missing, oversized, or failed falls back unchanged, and a flaky cross-PR test is fixed by capturing the lookup count before review-completion fanout.

New Features

  • Addressed judge resolves at p>=0.97 with visible evidence, keeps open at p<=0.05, and escalates the middle band to the LLM.
  • Convention relations skip the LLM only when every neighbor's 4-way choice is >=0.95; any uncertain answer escalates the whole batch.
  • Intent verification returns a verdict only on a confident all-clear across delivers, acceptance criteria, and per-finding scope.
  • Scoring drops findings from the judge prompt only at fp>=0.97 plus defect<=0.5, then re-enters them as score-10 synthetic groups for the audit trail.
  • The triage shadow logs jev.shadow.triage agreement on up to 40 files; slow evals are abandoned without billing.
  • Jev receives only sanitized, delimiter-wrapped text; convention text that would truncate escalates to the LLM, and oversized-state evals emit jev.call.failed.
  • Manual model_pricing rows still win; models without one now use OpenRouter's public catalog, refreshed in the background, instead of costing $0.

Migration / Ops

  • Enable per installation with a stored typesafe provider key (repo-level then org-level) or with TYPESAFE_API_KEY plus the jev_classifier feature flag; no key means no Jev calls.
  • A stored key counts as consent to egress; the env-var path requires the flag because the shared key alone never sends tenant data.
  • Stored typesafe endpoints are validated and normalized on save; malformed URLs return a 400, and an invalid stored URL disables Jev instead of rerouting traffic.
  • Jev spend merges into existing stage token buckets; convention-relation spend is now billed on both legs.
  • Mixed buckets keep a per-leg Aux ledger, and stats/models reports per-model totals for every scalar stage.
  • Egress is limited to opted-in installs: sanitized finding text, PR metadata/body, intent, diff hunks, and stored conventions.
  • The OpenRouter catalog fallback is a public async fetch; disable it with OPENROUTER_PRICING_ENABLED=false in restricted-egress deployments.

Written for commit 8d2366a. Summary will update on new commits.

Review in cubic

Ports the Jev integration from argus-private (#301): TypeSafe's System One
eval API front-runs five typed-decision surfaces — addressed judge,
convention relations, intent verification, scoring FP pre-filter, and an
observe-only triage shadow — with the LLM as the fail-safe fallback on any
error, timeout, missing answer, or mid-band probability.

Credentials resolve per installation: a stored 'typesafe' provider key
(repo-level, then org-level; BYOK — the key IS the consent) else the env
TYPESAFE_API_KEY gated by the jev_classifier feature flag (default OFF).
Settings → Providers gains a TypeSafe Jev card beside Embeddings;
providers/settings pages get section landmarks, consistent card states,
and a whitelisted LLM provider count.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 31 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/internal/pipeline/triage.go">

<violation number="1" location="backend/internal/pipeline/triage.go:77">
P2: When a BYOK Jev key is configured, `startJevTriageShadow` resolves it synchronously before creating the shadow goroutine. The database lookup therefore blocks the LLM leg despite this path being documented as parallel; resolve the evaluator asynchronously or start both operations concurrently.</violation>
</file>

<file name="backend/internal/pipeline/jev_conventions.go">

<violation number="1" location="backend/internal/pipeline/jev_conventions.go:66">
P1: When stored convention text contains an inline prompt directive, `sanitizeUserInput` leaves it intact because its patterns require the start of a line. Sanitize both convention fields with the untrusted-memory sanitizer, or explicitly instruct Jev to treat the wrapped values as data, before allowing a confident relation to skip the LLM.</violation>
</file>

<file name="backend/internal/pipeline/jev_scoring.go">

<violation number="1" location="backend/internal/pipeline/jev_scoring.go:61">
P2: When Jev marks a finding as dropped, this map eventually removes it from `run.FileReviews`, so `indexComments` never creates its `review_comments` row. Preserve the finding as a suppressed row while excluding it from the GitHub inline output.</violation>

<violation number="2" location="backend/internal/pipeline/jev_scoring.go:77">
P1: A PR-controlled finding description or suggestion can inject instructions into Jev and make a real finding satisfy the drop condition, silently suppressing it. Sanitize and delimiter-wrap each finding before adding it to the classifier state.</violation>
</file>

<file name="backend/internal/jev/jev.go">

<violation number="1" location="backend/internal/jev/jev.go:275">
P2: An HTTP 200 response with negative usage counts passes `json.Unmarshal`, returns no error, and produces negative cost; malformed `answers` is also accepted. Validate the response schema and non-negative usage before logging the call as completed or billing it.</violation>
</file>

<file name="backend/internal/jev/jev_test.go">

<violation number="1" location="backend/internal/jev/jev_test.go:14">
P3: Two documented, safety-relevant client behaviors have no unit tests in this package: (1) the `maxStateBytes` (120KB) fail-fast — `Evaluate` must error on oversized state instead of issuing a doomed round-trip; (2) `Result.Noul`/`Result.Choice` rejecting NaN/±Inf/out-of-range probabilities — `encoding/json` decodes `1e400` to `+Inf` (range errors are swallowed for floats) and `1.7` decodes fine, so these guards are reachable and exist precisely because an out-of-range value must never clear a caller's threshold. Neither path is exercised anywhere in this PR (`rg maxStateBytes internal --glob '*_test.go'` and `rg IsInf internal --glob '*_test.go'` both return nothing; only the 1.7 case is covered indirectly via pipeline tests). Add table tests for the cap and for NaN/±Inf/out-of-range probabilities returning nil / ok=false.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

// conventions are user-controlled text persisted across reviews.
func jevConventionState(candidate string, neighbors []memory.PatternMatch) map[string]any {
state := map[string]any{
"candidate_convention": conventionPromptField("candidate_convention", util.Truncate(candidate, jevConventionFieldCap, false)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When stored convention text contains an inline prompt directive, sanitizeUserInput leaves it intact because its patterns require the start of a line. Sanitize both convention fields with the untrusted-memory sanitizer, or explicitly instruct Jev to treat the wrapped values as data, before allowing a confident relation to skip the LLM.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/pipeline/jev_conventions.go, line 66:

<comment>When stored convention text contains an inline prompt directive, `sanitizeUserInput` leaves it intact because its patterns require the start of a line. Sanitize both convention fields with the untrusted-memory sanitizer, or explicitly instruct Jev to treat the wrapped values as data, before allowing a confident relation to skip the LLM.</comment>

<file context>
@@ -0,0 +1,102 @@
+// conventions are user-controlled text persisted across reviews.
+func jevConventionState(candidate string, neighbors []memory.PatternMatch) map[string]any {
+	state := map[string]any{
+		"candidate_convention": conventionPromptField("candidate_convention", util.Truncate(candidate, jevConventionFieldCap, false)),
+	}
+	for i, n := range neighbors {
</file context>

Comment thread backend/internal/pipeline/jev_conventions.go
Comment thread backend/internal/pipeline/jev_intent.go Outdated
findings := make([]string, 0, len(allComments))
for i, ic := range allComments {
c := run.FileReviews[ic.fileIdx].Comments[ic.commentIdx]
findings = append(findings, scoringFindingText(i, run.FileReviews[ic.fileIdx].Path, c))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A PR-controlled finding description or suggestion can inject instructions into Jev and make a real finding satisfy the drop condition, silently suppressing it. Sanitize and delimiter-wrap each finding before adding it to the classifier state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/pipeline/jev_scoring.go, line 77:

<comment>A PR-controlled finding description or suggestion can inject instructions into Jev and make a real finding satisfy the drop condition, silently suppressing it. Sanitize and delimiter-wrap each finding before adding it to the classifier state.</comment>

<file context>
@@ -0,0 +1,131 @@
+	findings := make([]string, 0, len(allComments))
+	for i, ic := range allComments {
+		c := run.FileReviews[ic.fileIdx].Comments[ic.commentIdx]
+		findings = append(findings, scoringFindingText(i, run.FileReviews[ic.fileIdx].Path, c))
+	}
+	var pr strings.Builder
</file context>

Comment thread backend/internal/pipeline/triage.go Outdated

// Shadow Jev eval — fires in parallel with the LLM leg, joined below.
// Observe-only: its answers are logged, never used for routing.
shadowCh := ts.startJevTriageShadow(ctx, run)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a BYOK Jev key is configured, startJevTriageShadow resolves it synchronously before creating the shadow goroutine. The database lookup therefore blocks the LLM leg despite this path being documented as parallel; resolve the evaluator asynchronously or start both operations concurrently.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/pipeline/triage.go, line 77:

<comment>When a BYOK Jev key is configured, `startJevTriageShadow` resolves it synchronously before creating the shadow goroutine. The database lookup therefore blocks the LLM leg despite this path being documented as parallel; resolve the evaluator asynchronously or start both operations concurrently.</comment>

<file context>
@@ -69,6 +72,10 @@ func (ts *TriageStage) Execute(ctx context.Context, run *PipelineRun) (err error
 
+	// Shadow Jev eval — fires in parallel with the LLM leg, joined below.
+	// Observe-only: its answers are logged, never used for routing.
+	shadowCh := ts.startJevTriageShadow(ctx, run)
+
 	// Phase 2: LLM refinement — only for manageable file counts
</file context>

Comment thread backend/internal/pipeline/types.go Outdated
Comment thread backend/internal/pipeline/jev_triage_shadow.go Outdated
Comment thread backend/internal/pipeline/orchestrator.go
Comment thread backend/internal/jev/jev.go Outdated
Comment thread web/src/app/(dashboard)/providers/jev-card.tsx Outdated
Cubic review remediation on the Jev port:

- sanitize + delimiter-wrap all untrusted text entering Jev state
  (filenames, finding descriptions, suggestions, conventions via
  retrieved-memory sanitizer); escalate when convention text would
  be truncated rather than classify partial rules
- StageTokens.Aux: per-leg spend ledger so mixed Jev+LLM buckets
  keep per-model attribution; stats/models aggregates aux legs and
  now covers every scalar stage (intent, auto-resolve, acceptance,
  cross_pr, reply, simulations)
- triage shadow: async BYOK resolution off the caller path, cancel
  in-flight eval on join-budget expiry, drain raced results, never
  bill skipped/abandoned calls
- jev client: single state marshal, bounded response read, clamp
  negative usage, oversized-state telemetry
- resolver: validate stored base_url (http(s) + host), fall back to
  default endpoint with a warning
- providers page: show repo-scoped typesafe keys read-only, surface
  delete errors
- .env.example: opt-in UPDATE gains WHERE clause

Generated with [Devin](https://devin.ai)

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and 3 new issues found across 20 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/internal/pipeline/jev_conventions.go">

<violation number="1" location="backend/internal/pipeline/jev_conventions.go:82">
P3: The new oversize-escalation branch has no test coverage, unlike every other escalation path in this file (TestJevConventions_OneUncertainEscalatesWholeBatch, _ErrorEscalates, _InvalidChoiceEscalates). Add a test asserting that an oversized candidate or neighbor returns ok=false, escalates to the LLM with full text, and bills zero Jev spend.</violation>
</file>

<file name="backend/internal/pipeline/jev_resolver.go">

<violation number="1" location="backend/internal/pipeline/jev_resolver.go:82">
P2: A stored endpoint with a query or fragment passes this check, but `jev.Client.evaluate` appends `/v1/systemone` to the raw URL. The request then targets the wrong path and every BYOK evaluation escalates; reject query and fragment components or join URL paths structurally.</violation>

<violation number="2" location="backend/internal/pipeline/jev_resolver.go:82">
P3: `validJevBaseURL` accepts plaintext `http://` endpoints even though this URL receives the TypeSafe API key (Bearer header) and tenant PR/finding content. For the egress of credentials plus data, restrict the accepted scheme to `https` (self-hosted plaintext relays can be terminated behind an explicit TLS proxy instead).</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread backend/internal/api/handlers_org_stats.go Outdated
// hosts, paths without scheme, non-http schemes) is rejected.
func validJevBaseURL(raw string) bool {
u, err := url.Parse(raw)
return err == nil && (u.Scheme == "https" || u.Scheme == "http") && u.Host != ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A stored endpoint with a query or fragment passes this check, but jev.Client.evaluate appends /v1/systemone to the raw URL. The request then targets the wrong path and every BYOK evaluation escalates; reject query and fragment components or join URL paths structurally.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/pipeline/jev_resolver.go, line 82:

<comment>A stored endpoint with a query or fragment passes this check, but `jev.Client.evaluate` appends `/v1/systemone` to the raw URL. The request then targets the wrong path and every BYOK evaluation escalates; reject query and fragment components or join URL paths structurally.</comment>

<file context>
@@ -66,6 +74,14 @@ func resolveJevEvaluator(ctx context.Context, keys jevKeyResolver, env jevEvalua
+// hosts, paths without scheme, non-http schemes) is rejected.
+func validJevBaseURL(raw string) bool {
+	u, err := url.Parse(raw)
+	return err == nil && (u.Scheme == "https" || u.Scheme == "http") && u.Host != ""
+}
+
</file context>

Comment thread backend/internal/pipeline/orchestrator.go
// A convention longer than the field cap would be truncated in state —
// a confident verdict on partial text could supersede or file a conflict
// on an incomplete rule. Escalate instead; the LLM prompt sees full text.
if len(candidate) > jevConventionFieldCap {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new oversize-escalation branch has no test coverage, unlike every other escalation path in this file (TestJevConventions_OneUncertainEscalatesWholeBatch, _ErrorEscalates, _InvalidChoiceEscalates). Add a test asserting that an oversized candidate or neighbor returns ok=false, escalates to the LLM with full text, and bills zero Jev spend.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/pipeline/jev_conventions.go, line 82:

<comment>The new oversize-escalation branch has no test coverage, unlike every other escalation path in this file (TestJevConventions_OneUncertainEscalatesWholeBatch, _ErrorEscalates, _InvalidChoiceEscalates). Add a test asserting that an oversized candidate or neighbor returns ok=false, escalates to the LLM with full text, and bills zero Jev spend.</comment>

<file context>
@@ -76,6 +76,17 @@ func jevConventionState(candidate string, neighbors []memory.PatternMatch) map[s
+	// A convention longer than the field cap would be truncated in state —
+	// a confident verdict on partial text could supersede or file a conflict
+	// on an incomplete rule. Escalate instead; the LLM prompt sees full text.
+	if len(candidate) > jevConventionFieldCap {
+		return nil, StageTokens{}, false
+	}
</file context>

Comment thread backend/internal/jev/jev_test.go Outdated
Comment thread backend/internal/pipeline/auto_resolve_tokens_test.go
Comment thread backend/internal/jev/jev.go Outdated
Comment thread backend/internal/pipeline/jev_triage_shadow_test.go Outdated
// hosts, paths without scheme, non-http schemes) is rejected.
func validJevBaseURL(raw string) bool {
u, err := url.Parse(raw)
return err == nil && (u.Scheme == "https" || u.Scheme == "http") && u.Host != ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: validJevBaseURL accepts plaintext http:// endpoints even though this URL receives the TypeSafe API key (Bearer header) and tenant PR/finding content. For the egress of credentials plus data, restrict the accepted scheme to https (self-hosted plaintext relays can be terminated behind an explicit TLS proxy instead).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/pipeline/jev_resolver.go, line 82:

<comment>`validJevBaseURL` accepts plaintext `http://` endpoints even though this URL receives the TypeSafe API key (Bearer header) and tenant PR/finding content. For the egress of credentials plus data, restrict the accepted scheme to `https` (self-hosted plaintext relays can be terminated behind an explicit TLS proxy instead).</comment>

<file context>
@@ -66,6 +74,14 @@ func resolveJevEvaluator(ctx context.Context, keys jevKeyResolver, env jevEvalua
+// hosts, paths without scheme, non-http schemes) is rejected.
+func validJevBaseURL(raw string) bool {
+	u, err := url.Parse(raw)
+	return err == nil && (u.Scheme == "https" || u.Scheme == "http") && u.Host != ""
+}
+
</file context>

Comment thread backend/internal/pipeline/jev_resolver.go Outdated
model_pricing misses previously costed $0. Now the lookup chain falls
back to OpenRouter's public /models catalog (no auth): manual rows
still win, then exact id → unique suffix → prefix match. Catalog is
cached 6h, failed fetches serve stale and throttle retries per TTL.
Ambiguous suffix matches never guess a price.

Generated with [Devin](https://devin.ai)

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/internal/app/app.go">

<violation number="1" location="backend/internal/app/app.go:167">
P2: When a review uses a non-OpenRouter or custom OpenAI-compatible provider without a manual pricing row, this fallback still assigns an OpenRouter catalog price because the callback cannot see the provider. Restrict the catalog fallback to OpenRouter calls or make cost lookup provider-aware; otherwise cost telemetry and billing are incorrect.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread backend/internal/app/app.go Outdated
if in, out, ok := pricingCache.Lookup(ctx, model); ok {
return in, out, true
}
return openRouterPricing.Lookup(model)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a review uses a non-OpenRouter or custom OpenAI-compatible provider without a manual pricing row, this fallback still assigns an OpenRouter catalog price because the callback cannot see the provider. Restrict the catalog fallback to OpenRouter calls or make cost lookup provider-aware; otherwise cost telemetry and billing are incorrect.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/app/app.go, line 167:

<comment>When a review uses a non-OpenRouter or custom OpenAI-compatible provider without a manual pricing row, this fallback still assigns an OpenRouter catalog price because the callback cannot see the provider. Restrict the catalog fallback to OpenRouter calls or make cost lookup provider-aware; otherwise cost telemetry and billing are incorrect.</comment>

<file context>
@@ -154,11 +154,17 @@ func Run() error {
+		if in, out, ok := pricingCache.Lookup(ctx, model); ok {
+			return in, out, true
+		}
+		return openRouterPricing.Lookup(model)
 	})
 	logger.InfoContext(ctx, "pricing cache initialization completed", "cache_ttl", 10*time.Minute)
</file context>

Comment thread backend/internal/llm/openrouter_pricing.go Outdated
Comment thread backend/internal/llm/openrouter_pricing.go Outdated
Comment thread backend/internal/llm/openrouter_pricing_test.go
Comment thread backend/internal/app/app.go Outdated
- handlers_config: reject malformed typesafe base_url with 400
- jev: export ValidBaseURL shared by resolver + config handler
- jev: emit jev.call.failed on oversized state for failure parity
- config: firstNonBlankEnv trims TYPESAFE_API_KEY before fallback
- triage: injectable join/drain budgets — tests use ms windows
- tests: conventions missing-answer escalation, intent Diff fixture,
  scoring defect_2 below veto, NaN/Inf answer guards, resolver atomics

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and 3 new issues found across 12 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/internal/jev/jev_test.go">

<violation number="1" location="backend/internal/jev/jev_test.go:176">
P3: The comment's premise is incorrect: encoding/json returns an UnmarshalTypeError whenever strconv.ParseFloat overflows float64, so `1e400` would make Evaluate error-out and escalate to the LLM fallback — the Noul/Choice range guard never sees it. The added Inf cases still validly exercise the guards (defense in depth for directly constructed Results), but the comment misstates the threat model and should be corrected so readers don't believe a hostile endpoint can deliver +Inf probabilities untouched.</violation>
</file>

<file name="backend/internal/pipeline/jev_resolver_test.go">

<violation number="1" location="backend/internal/pipeline/jev_resolver_test.go:230">
P3: In TestResolveCandidates_BYOKSelectsJevCascade, the failure message formats the atomic.Value itself instead of its payload. %q on an atomic.Value prints the unexported struct contents, so when this test fails the message shows no actual auth header — the one value the test exists to verify. Change %q's operand to auth.Load().</violation>
</file>

<file name="backend/internal/pipeline/jev_triage_shadow_test.go">

<violation number="1" location="backend/internal/pipeline/jev_triage_shadow_test.go:193">
P3: The millisecond budgets leave ~20ms of wall-clock headroom, which can flake under a loaded `-race`/CI runner. `finishJevTriageShadow` realistically waits join+drainBudget (≈40ms), but the cap is `2*join` (60ms) — two back-to-back timer overshoots of ~10ms each fail the assertion. The delay (2*join=60ms) is also only 20ms past join+drain (40ms), so the abandon ordering relies on timer accuracy. Since the fake's Evaluate is aborted by the cancel, a much longer delay adds no wall time: widen the margins so only an order-of-magnitude skew can flake.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment on lines +176 to +177
// 1e400 decodes to +Inf without a json error — the range guard is
// the only thing stopping it clearing a caller's threshold.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The comment's premise is incorrect: encoding/json returns an UnmarshalTypeError whenever strconv.ParseFloat overflows float64, so 1e400 would make Evaluate error-out and escalate to the LLM fallback — the Noul/Choice range guard never sees it. The added Inf cases still validly exercise the guards (defense in depth for directly constructed Results), but the comment misstates the threat model and should be corrected so readers don't believe a hostile endpoint can deliver +Inf probabilities untouched.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/jev/jev_test.go, line 176:

<comment>The comment's premise is incorrect: encoding/json returns an UnmarshalTypeError whenever strconv.ParseFloat overflows float64, so `1e400` would make Evaluate error-out and escalate to the LLM fallback — the Noul/Choice range guard never sees it. The added Inf cases still validly exercise the guards (defense in depth for directly constructed Results), but the comment misstates the threat model and should be corrected so readers don't believe a hostile endpoint can deliver +Inf probabilities untouched.</comment>

<file context>
@@ -166,28 +166,35 @@ func TestEvaluate_OversizedResponseErrors(t *testing.T) {
+		"out_high": {Type: TypeNoul, Noul: ptr(1.01)},
+		"out_neg":  {Type: TypeNoul, Noul: ptr(-0.01)},
+		"nan":      {Type: TypeNoul, Noul: &nan},
+		// 1e400 decodes to +Inf without a json error — the range guard is
+		// the only thing stopping it clearing a caller's threshold.
+		"pos_inf":      {Type: TypeNoul, Noul: &posInf},
</file context>
Suggested change
// 1e400 decodes to +Inf without a json error — the range guard is
// the only thing stopping it clearing a caller's threshold.
// Defense in depth: encoding/json already errors on floats that
// overflow float64 (ParseFloat ErrRange), so +Inf cannot reach the
// guard from the wire; it still rejects directly-constructed results.

if hits.Load() != 1 {
t.Fatalf("BYOK eval must hit the stored endpoint once, got %d", hits.Load())
}
if auth.Load() != "Bearer ts-byok" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: In TestResolveCandidates_BYOKSelectsJevCascade, the failure message formats the atomic.Value itself instead of its payload. %q on an atomic.Value prints the unexported struct contents, so when this test fails the message shows no actual auth header — the one value the test exists to verify. Change %q's operand to auth.Load().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/pipeline/jev_resolver_test.go, line 230:

<comment>In TestResolveCandidates_BYOKSelectsJevCascade, the failure message formats the atomic.Value itself instead of its payload. %q on an atomic.Value prints the unexported struct contents, so when this test fails the message shows no actual auth header — the one value the test exists to verify. Change %q's operand to auth.Load().</comment>

<file context>
@@ -222,10 +224,10 @@ func TestResolveCandidates_BYOKSelectsJevCascade(t *testing.T) {
+		t.Fatalf("BYOK eval must hit the stored endpoint once, got %d", hits.Load())
 	}
-	if auth != "Bearer ts-byok" {
+	if auth.Load() != "Bearer ts-byok" {
 		t.Fatalf("stored key must auth the eval, got %q", auth)
 	}
</file context>

start := time.Now()
ts.finishJevTriageShadow(context.Background(), run, s,
map[string]TriageResult{"f0.go": {File: "f0.go", Action: TriageDeep}})
if elapsed := time.Since(start); elapsed >= 2*join {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The millisecond budgets leave ~20ms of wall-clock headroom, which can flake under a loaded -race/CI runner. finishJevTriageShadow realistically waits join+drainBudget (≈40ms), but the cap is 2*join (60ms) — two back-to-back timer overshoots of ~10ms each fail the assertion. The delay (2*join=60ms) is also only 20ms past join+drain (40ms), so the abandon ordering relies on timer accuracy. Since the fake's Evaluate is aborted by the cancel, a much longer delay adds no wall time: widen the margins so only an order-of-magnitude skew can flake.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/pipeline/jev_triage_shadow_test.go, line 193:

<comment>The millisecond budgets leave ~20ms of wall-clock headroom, which can flake under a loaded `-race`/CI runner. `finishJevTriageShadow` realistically waits join+drainBudget (≈40ms), but the cap is `2*join` (60ms) — two back-to-back timer overshoots of ~10ms each fail the assertion. The delay (2*join=60ms) is also only 20ms past join+drain (40ms), so the abandon ordering relies on timer accuracy. Since the fake's Evaluate is aborted by the cancel, a much longer delay adds no wall time: widen the margins so only an order-of-magnitude skew can flake.</comment>

<file context>
@@ -175,17 +175,22 @@ func TestJevTriageShadow_ErrorStampsNoProvider(t *testing.T) {
 	ts.finishJevTriageShadow(context.Background(), run, s,
 		map[string]TriageResult{"f0.go": {File: "f0.go", Action: TriageDeep}})
-	if elapsed := time.Since(start); elapsed >= 2*jevShadowJoinBudget {
+	if elapsed := time.Since(start); elapsed >= 2*join {
 		t.Fatalf("join blocked %v, want <= join budget", elapsed)
 	}
</file context>

- jev.ValidBaseURL: reject query/fragment/userinfo; http only for
  loopback/private/link-local hosts
- resolver: invalid stored base_url fails closed (no evaluator) instead
  of silently rerouting tenant data to the default endpoint
- jev: check status before size so large error bodies surface the API's
  real message; cost-only stages counted in stats/models
- conventions: fold Jev + LLM legs into Aux separately on escalation;
  foldAuxTokens flattens nested ledgers
- openrouter pricing: async refresh (Lookup never blocks), cancellable
  ctx, 5min failure retry vs 6h TTL, NaN/Inf price guards, opt-out env
  OPENROUTER_PRICING_ENABLED
- tests: oversize escalation w/ full-text assert, "too large" error pin,
  headline-after-fold pin, join-exact shadow timing, NaN/Inf rows
@BeLazy167

Copy link
Copy Markdown
Owner Author

Disposition on the three review runs (244ef3cd re-review, 1111b7b1, 2a146647).

Fixed in this push (9eb4071)

Run 1111b7b1:

  • statsModels: cost-only stages (tokens=0, cost>0) now aggregate — gate matches foldAuxTokens.
  • ValidBaseURL: rejects query/fragment/userinfo components (a stored ?x= would retarget /v1/systemone); plain http restricted to loopback/private/link-local hosts since the URL carries a Bearer credential + tenant content.
  • Resolver fails closed: an invalid stored base_url now disables the evaluator with a warning instead of silently rerouting tenant data to api.typesafe.ai.
  • evaluate: status check runs before the size check — a large non-200 body surfaces the API's real error text (read is still bounded).
  • classifyConventionRelations: Jev and LLM legs fold into Aux separately on escalation; foldAuxTokens flattens nested ledgers so per-leg provenance survives into the bucket.
  • Oversize-escalation branch now tested (TestJevConventions_OversizedFieldEscalatesWithFullText — asserts Jev skipped, LLM sees FULL text, only the LLM leg bills).
  • TestEvaluate_OversizedResponseErrors asserts the explicit too large error, pinning the bounded-read path.
  • TestFoldAuxTokens_MixedBucketSemantics asserts the first-writer headline survives the second fold before the explicit overwrite.
  • TestJevTriageShadow_LateResultStillBilled delivers at exactly the join budget — either select branch bills; no wall-clock margin.

Run 2a146647 (pricing):

  • Catalog rows with NaN/Inf price strings are rejected (ParseFloat accepts them without error).
  • Lookup never blocks the cost path: refresh is single-flighted + async (Warm at startup, maybeRefresh on staleness), failures retry every 5min instead of disabling for the 6h TTL, and LookupCtx carries the app ctx so fetches cancel on shutdown.
  • New OPENROUTER_PRICING_ENABLED env (default true, documented in .env.example) — opt-out for restricted-egress installs.
  • Suffix/prefix lookup tests now assert the completion price too.

Stale — already fixed before filing (run 244ef3cd leftovers)

  • Conventions use sanitizeRetrievedMemory (unanchored) in conventionPromptField; oversized fields escalate instead of truncating.
  • Scoring finding text goes through wrapSafeDelimiters (sanitize + tag-scrub + delimiters) for both judge prompt and Jev state.
  • BYOK resolution happens inside the shadow goroutine — startJevTriageShadow never blocks the LLM leg.
  • Dropped findings re-enter as synthetic low-score groups — indexComments persists them as suppressed rows.
  • Negative usage counts clamp to zero in evaluate.
  • maxStateBytes fail-fast and NaN/±Inf/out-of-range probability guards are unit-tested.

One intentional design decision (not a defect)

4052413004: the OpenRouter catalog fallback intentionally applies to any model without a manual model_pricing row, regardless of routing provider — that is the feature's stated purpose (free public pricing so installs skip manual rows). OR list prices are estimates for cost reporting, not billing; a manual row always wins when precision matters. Threading provider through would gate the fallback to openrouter-routed calls only and make it useless for gateway-routed deployments.

Outstanding

internal/llm/chat.go unbounded io.ReadAll(resp.Body) — same class as the bounded-read fix, pre-existing on both repos, outside this PR's scope. Worth a follow-up.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 existing issues remain and 3 new issues found across 15 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/internal/api/handlers_org_stats.go">

<violation number="1" location="backend/internal/api/handlers_org_stats.go:222">
P3: The relaxed skip condition in addModel (stats/models) has no test pinning it, while the identical cost-only fix in the stage-costs aggregator is covered by TestAggregateStageCosts_KeepsCostOnlyStage. No test in the repo references statsModels/addModel/StatsModels, so a future change that reverts this condition (e.g., back to gating on TotalTokens alone) would silently drop cost-only spend from the dashboard's per-model stats with no failing test. Add a test that feeds a RunTokenUsage with {TotalTokens: 0, Cost > 0, Model set} and asserts the model appears with that cost, mirroring the existing stage-costs test.</violation>
</file>

<file name="backend/internal/llm/openrouter_pricing.go">

<violation number="1" location="backend/internal/llm/openrouter_pricing.go:67">
P2: When the first OpenRouter-only completion finishes before the background warm fetch completes, `LookupCtx` returns not-found and `EstimateCost` records a zero cost; the later refresh cannot repair that completed usage record. Keep a bounded initial fetch in the pricing path, or defer/recompute cost after the in-flight refresh completes.</violation>
</file>

<file name="backend/internal/pipeline/types.go">

<violation number="1" location="backend/internal/pipeline/types.go:536">
P2: The flatten only preserves per-leg provenance in memory; the persistence path for the AutoResolve bucket still collapses it. `resolveCandidates` passes the summed bucket (`stats.judgeTokens = tokens.AutoResolve`, now carrying one Aux entry per judge call — Jev legs + LLM escalation) to `persistAsyncStageTokens`, which marshals it as `entry` and merges via `MergeStageTokenEntry`. That SQL does `jsonb_build_array($2::jsonb - 'aux')` — it strips the entry's own aux and appends the bucket as one headline-stamped entry. Since auto-resolve persists with `run == nil` (`persistAsyncStageTokens(..., nil)`), the in-memory copy the fold feeds doesn't exist on that path, so the per-leg split the new fold exists to preserve never reaches the reviews.token_usage row that /stats and the per-review TokenPill read. Make the DB merge append the leg entries (or have the caller flatten before persisting) so the stored AutoResolve bucket keeps per-model attribution; the current head stores only the aggregate plus a single stamped entry.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic


// Warm kicks an asynchronous catalog fetch — startup warming only; the fetch
// itself is throttled and single-flighted by maybeRefresh.
func (o *OpenRouterPricing) Warm(ctx context.Context) { o.maybeRefresh(ctx) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the first OpenRouter-only completion finishes before the background warm fetch completes, LookupCtx returns not-found and EstimateCost records a zero cost; the later refresh cannot repair that completed usage record. Keep a bounded initial fetch in the pricing path, or defer/recompute cost after the in-flight refresh completes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/llm/openrouter_pricing.go, line 67:

<comment>When the first OpenRouter-only completion finishes before the background warm fetch completes, `LookupCtx` returns not-found and `EstimateCost` records a zero cost; the later refresh cannot repair that completed usage record. Keep a bounded initial fetch in the pricing path, or defer/recompute cost after the in-flight refresh completes.</comment>

<file context>
@@ -53,28 +62,14 @@ type openRouterCatalogEntry struct {
-	defer cancel()
+// Warm kicks an asynchronous catalog fetch — startup warming only; the fetch
+// itself is throttled and single-flighted by maybeRefresh.
+func (o *OpenRouterPricing) Warm(ctx context.Context) { o.maybeRefresh(ctx) }
+
+// Refresh fetches the catalog synchronously and swaps the price map on
</file context>

// A bucket folded into a bucket flattens its legs — a caller that already
// mixed providers (Jev leg + LLM escalation) returns per-leg provenance
// that must survive, not collapse into one headline-stamped entry.
if len(spend.Aux) > 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The flatten only preserves per-leg provenance in memory; the persistence path for the AutoResolve bucket still collapses it. resolveCandidates passes the summed bucket (stats.judgeTokens = tokens.AutoResolve, now carrying one Aux entry per judge call — Jev legs + LLM escalation) to persistAsyncStageTokens, which marshals it as entry and merges via MergeStageTokenEntry. That SQL does jsonb_build_array($2::jsonb - 'aux') — it strips the entry's own aux and appends the bucket as one headline-stamped entry. Since auto-resolve persists with run == nil (persistAsyncStageTokens(..., nil)), the in-memory copy the fold feeds doesn't exist on that path, so the per-leg split the new fold exists to preserve never reaches the reviews.token_usage row that /stats and the per-review TokenPill read. Make the DB merge append the leg entries (or have the caller flatten before persisting) so the stored AutoResolve bucket keeps per-model attribution; the current head stores only the aggregate plus a single stamped entry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/pipeline/types.go, line 536:

<comment>The flatten only preserves per-leg provenance in memory; the persistence path for the AutoResolve bucket still collapses it. `resolveCandidates` passes the summed bucket (`stats.judgeTokens = tokens.AutoResolve`, now carrying one Aux entry per judge call — Jev legs + LLM escalation) to `persistAsyncStageTokens`, which marshals it as `entry` and merges via `MergeStageTokenEntry`. That SQL does `jsonb_build_array($2::jsonb - 'aux')` — it strips the entry's own aux and appends the bucket as one headline-stamped entry. Since auto-resolve persists with `run == nil` (`persistAsyncStageTokens(..., nil)`), the in-memory copy the fold feeds doesn't exist on that path, so the per-leg split the new fold exists to preserve never reaches the reviews.token_usage row that /stats and the per-review TokenPill read. Make the DB merge append the leg entries (or have the caller flatten before persisting) so the stored AutoResolve bucket keeps per-model attribution; the current head stores only the aggregate plus a single stamped entry.</comment>

<file context>
@@ -530,8 +530,14 @@ func foldAuxTokens(bucket *StageTokens, spend StageTokens) {
+	// A bucket folded into a bucket flattens its legs — a caller that already
+	// mixed providers (Jev leg + LLM escalation) returns per-leg provenance
+	// that must survive, not collapse into one headline-stamped entry.
+	if len(spend.Aux) > 0 {
+		bucket.Aux = append(bucket.Aux, spend.Aux...)
+	} else {
</file context>

}
return
}
if st.Model == "" || (st.TotalTokens == 0 && st.Cost == 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The relaxed skip condition in addModel (stats/models) has no test pinning it, while the identical cost-only fix in the stage-costs aggregator is covered by TestAggregateStageCosts_KeepsCostOnlyStage. No test in the repo references statsModels/addModel/StatsModels, so a future change that reverts this condition (e.g., back to gating on TotalTokens alone) would silently drop cost-only spend from the dashboard's per-model stats with no failing test. Add a test that feeds a RunTokenUsage with {TotalTokens: 0, Cost > 0, Model set} and asserts the model appears with that cost, mirroring the existing stage-costs test.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/internal/api/handlers_org_stats.go, line 222:

<comment>The relaxed skip condition in addModel (stats/models) has no test pinning it, while the identical cost-only fix in the stage-costs aggregator is covered by TestAggregateStageCosts_KeepsCostOnlyStage. No test in the repo references statsModels/addModel/StatsModels, so a future change that reverts this condition (e.g., back to gating on TotalTokens alone) would silently drop cost-only spend from the dashboard's per-model stats with no failing test. Add a test that feeds a RunTokenUsage with {TotalTokens: 0, Cost > 0, Model set} and asserts the model appears with that cost, mirroring the existing stage-costs test.</comment>

<file context>
@@ -219,7 +219,7 @@ func (s *Server) statsModels(w http.ResponseWriter, r *http.Request) {
 			return
 		}
-		if st.Model == "" || st.TotalTokens == 0 {
+		if st.Model == "" || (st.TotalTokens == 0 && st.Cost == 0) {
 			return
 		}
</file context>

waitSiblingFanoutSettled read the lookup counter after OnReviewCompleted
had already spawned the fanout goroutine. When the lookup landed first,
the helper waited for a count that had already passed and burned the
full deadline. Capture the baseline before triggering the fanout.
@BeLazy167
BeLazy167 merged commit 1c06a50 into main Sep 19, 2026
5 checks passed
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.

1 participant