Skip to content

feat: add iii-eval worker - #10

Merged
rohitg00 merged 7 commits into
mainfrom
feat/eval
Apr 22, 2026
Merged

feat: add iii-eval worker#10
rohitg00 merged 7 commits into
mainfrom
feat/eval

Conversation

@rohitg00

@rohitg00 rohitg00 commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • OTel-native evaluation worker with 7 functions
  • Ingest spans, compute P50/P95/P99 metrics, score system health, detect drift, analyze traces
  • All through iii primitives: State, Cron, PubSub Subscribe, HTTP triggers
  • 19 tests, 0 warnings

Functions

Function Description
eval::ingest Receive span data via PubSub telemetry.spans
eval::metrics P50/P95/P99 latency, success rate, throughput
eval::score System-wide health score (0-100)
eval::drift Detect metric drift against baselines
eval::baseline Snapshot current metrics
eval::report Full report: metrics + score + drift
eval::analyze_traces Read OTel traces, find patterns, generate insights

Test plan

  • cargo test — 19 tests passing
  • cargo check — 0 warnings
  • E2E tested with engine v0.10.0 + console chat bar

Summary by CodeRabbit

  • New Features

    • Added an evaluation worker that ingests telemetry spans, computes per-function metrics (latency percentiles, success rate, throughput), produces an overall health score (0–100), and supports baselining and drift detection with periodic checks and PubSub ingestion.
    • Added CLI and HTTP endpoints to run reports and analyze traces; configurable via a config file.
  • Documentation

    • Added detailed README and SPEC with setup, config examples, and usage instructions.

@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@rohitg00 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 43 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 46 minutes and 43 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a2740c14-1be9-452e-a5a1-41a44202d204

📥 Commits

Reviewing files that changed from the base of the PR and between be30020 and eee976f.

📒 Files selected for processing (10)
  • eval/.gitignore
  • eval/Cargo.toml
  • eval/README.md
  • eval/src/config.rs
  • eval/src/functions/analyze.rs
  • eval/src/functions/baseline.rs
  • eval/src/functions/drift.rs
  • eval/src/functions/ingest.rs
  • eval/src/functions/score.rs
  • eval/src/main.rs
📝 Walkthrough

Walkthrough

A new evaluation worker crate iii-eval is added under eval/, providing a Rust binary that ingests OpenTelemetry spans, persists per-function spans, computes metrics, detects drift versus baselines, and produces health scores and reports via registered III engine functions and triggers.

Changes

Cohort / File(s) Summary
Cargo & Build
eval/Cargo.toml, eval/build.rs
New crate manifest iii-eval with runtime and telemetry-related dependencies; build script exports TARGET into build-time env.
Configuration & Manifest
eval/config.yaml, eval/src/config.rs, eval/src/manifest.rs
Default YAML config added; EvalConfig struct with serde defaults and loader; build_manifest() producing module metadata and default_config.
Binary Entrypoint
eval/src/main.rs
Tokio async CLI binary that loads config, registers functions, registers worker with OTel, and sets up cron, pubsub, and HTTP triggers.
Function Registry
eval/src/functions/mod.rs
Registers seven evaluation functions with the III engine and wires state index updates after ingest.
State Helpers
eval/src/functions/state.rs
state_get / state_set helpers invoking state::get and state::set via iii.trigger.
Ingestion & Storage
eval/src/functions/ingest.rs
Ingest handler extracts required fields, timestamps, appends spans to per-function state, enforces max_spans_per_function, and updates function index.
Metrics Computation
eval/src/functions/metrics.rs
compute_metrics computes p50/p95/p99, success rate, avg duration, error counts, throughput per minute; also provides a handler reading stored spans.
Baseline Management
eval/src/functions/baseline.rs
Handler snapshots current metrics into eval:baselines:{function_id} with RFC3339 timestamp and persists baseline.
Drift Detection
eval/src/functions/drift.rs
Handler compares current metrics to saved baselines across dimensions, computes relative delta_pct with zero-baseline handling, and returns per-function drift results using drift_threshold.
Scoring & Reporting
eval/src/functions/score.rs, eval/src/functions/report.rs
score aggregates per-function deductions into a 0–100 overall score with issues/suggestions; report composes per-function metrics, optional drift, and overall score.
Trace Analysis
eval/src/functions/analyze.rs
Handler that queries traces via iii.trigger, extracts spans across shapes, aggregates per-function stats, ranks slowest/most-active functions, and returns insights.
Docs & Spec
eval/README.md, eval/SPEC.md
README and SPEC describing architecture, APIs (ingest, metrics, score, drift, baseline, report, analyze), state keys, triggers (cron, PubSub, HTTP), config schema, and usage examples.

Sequence Diagram(s)

sequenceDiagram
    participant PubSub
    participant III as III Engine
    participant Handlers
    participant State

    PubSub->>III: Publish telemetry.spans
    III->>Handlers: Trigger eval::ingest
    Handlers->>State: state_get(eval:spans, fid)
    Handlers->>State: state_set(eval:spans, fid, updated_spans)
    State-->>Handlers: ack
    Handlers-->>III: ingestion result
Loading
sequenceDiagram
    participant Client
    participant III as III Engine
    participant Handlers
    participant State
    participant Cron

    Client->>III: POST /eval/metrics (or eval::metrics)
    III->>Handlers: Trigger eval::metrics
    Handlers->>State: state_get(eval:spans, fid)
    Handlers->>Handlers: compute_metrics → p50/p95/p99, rates, throughput
    Handlers-->>III: metrics JSON

    Cron->>III: Cron trigger
    III->>Handlers: Trigger eval::drift
    Handlers->>State: state_get(eval:baselines, fid)
    Handlers->>State: state_get(eval:spans, fid)
    Handlers->>Handlers: compare baseline vs current, compute delta_pct
    Handlers-->>III: drift results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • sergiofilhowz

Poem

🐰 Whiskers twitch with glee,
Seven handlers hop and see,
Spans arrive and baselines keep,
Drift is found from data deep,
Scores and reports — a carrot treat! 🎋

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add iii-eval worker' directly and clearly describes the main change: introducing a new evaluation worker module called iii-eval with seven functions for OTel span processing, metrics computation, health scoring, drift detection, and reporting.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/eval

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

OTel-native evaluation worker for iii engine. 7 functions:
- eval::ingest — receive span data via PubSub
- eval::metrics — P50/P95/P99 latency, success rate, throughput
- eval::score — system-wide health score (0-100)
- eval::drift — detect metric drift against baselines
- eval::baseline — snapshot current metrics
- eval::report — full report with metrics + score + drift
- eval::analyze_traces — read OTel traces, find patterns, generate insights

Uses iii primitives: State (spans, baselines), Cron (drift check), Subscribe (telemetry.spans), HTTP triggers.
19 tests, 0 warnings.

@coderabbitai coderabbitai 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.

Actionable comments posted: 8

🧹 Nitpick comments (12)
eval/src/manifest.rs (1)

17-23: Avoid duplicating defaults between manifest and runtime config.

default_config is hardcoded here; it can drift from EvalConfig::default() over time.

Proposed fix
+use crate::config::EvalConfig;
+
 pub fn build_manifest() -> ModuleManifest {
     ModuleManifest {
         name: env!("CARGO_PKG_NAME").to_string(),
         version: env!("CARGO_PKG_VERSION").to_string(),
         description: "III engine OTel-native evaluation worker".to_string(),
-        default_config: serde_json::json!({
-            "retention_hours": 24,
-            "drift_threshold": 0.15,
-            "cron_drift_check": "0 */10 * * * *",
-            "max_spans_per_function": 1000,
-            "baseline_window_minutes": 60
-        }),
+        default_config: serde_json::to_value(EvalConfig::default())
+            .expect("EvalConfig should always serialize"),
         supported_targets: vec![env!("TARGET").to_string()],
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/manifest.rs` around lines 17 - 23, The manifest hardcodes
default_config JSON that duplicates EvalConfig::default(), causing drift;
replace the hardcoded serde_json::json! block with a conversion from
EvalConfig::default() (e.g., serialize or convert EvalConfig::default() into a
serde_json::Value) so default_config is derived from EvalConfig::default()
instead of duplicated; update the code that constructs default_config (the
variable named default_config in this diff) to call EvalConfig::default() and
serialize/convert it into the same JSON/Value shape used elsewhere.
eval/src/config.rs (1)

51-54: Validate semantic ranges after deserialization.

load_config should enforce constraints (for example: drift_threshold in [0.0, 1.0], non-zero max_spans_per_function) before returning config.

Proposed fix
 pub fn load_config(path: &str) -> Result<EvalConfig> {
     let contents = std::fs::read_to_string(path)?;
     let config: EvalConfig = serde_yaml::from_str(&contents)?;
+    if !(0.0..=1.0).contains(&config.drift_threshold) {
+        anyhow::bail!("drift_threshold must be between 0.0 and 1.0");
+    }
+    if config.max_spans_per_function == 0 {
+        anyhow::bail!("max_spans_per_function must be > 0");
+    }
     Ok(config)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/config.rs` around lines 51 - 54, After deserializing into EvalConfig
in load_config, validate semantic constraints before returning: check that
EvalConfig.drift_threshold is within 0.0..=1.0 and that
EvalConfig.max_spans_per_function is > 0 (and add any other domain rules
required by EvalConfig); if any check fails return an Err with a clear message
(e.g., using anyhow::bail or Result::Err with a descriptive string) instead of
Ok(config). Ensure the validation lives in load_config immediately after
serde_yaml::from_str so callers never receive an out-of-range EvalConfig.
eval/src/functions/report.rs (2)

35-42: Silent error swallowing may hide drift detection failures.

Using .ok() to ignore drift handler errors means failures (e.g., state read errors, serialization issues) are silently discarded. Consider logging drift failures or including a drift_error field in the report for observability.

Suggested improvement
         let drift_result = if has_baseline {
             let drift_payload = json!({ "function_id": fid });
-            crate::functions::drift::handle(iii, config, drift_payload)
-                .await
-                .ok()
+            match crate::functions::drift::handle(iii, config, drift_payload).await {
+                Ok(result) => Some(result),
+                Err(e) => {
+                    tracing::warn!(function_id = %fid, error = %e, "drift check failed");
+                    None
+                }
+            }
         } else {
             None
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/report.rs` around lines 35 - 42, Replace the silent .ok()
drop of errors when calling crate::functions::drift::handle so drift failures
are observable: change the handling around the drift_result (the block using
has_baseline, drift_payload, and the call to
crate::functions::drift::handle(iii, config, drift_payload)) to capture the
Result, log the error via the existing logger or attach an error string to the
report (e.g., a drift_error field) instead of returning None, and ensure success
still yields the drift payload; this preserves the existing flow but surfaces
errors for observability.

18-50: Per-function drift calls are inefficient when checking all functions.

The loop calls drift::handle individually for each function with a baseline. However, drift::handle already supports processing all functions when function_id is omitted from the payload. Consider a single drift call for the full report, then matching results by function_id.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/report.rs` around lines 18 - 50, The per-function loop
currently calls crate::functions::drift::handle(...) for each fid; instead, call
crate::functions::drift::handle once outside the loop with a payload that omits
"function_id" to get drift results for all functions, then inside the loop
lookup the drift entry for each fid (e.g., map by function_id) when building
function_reports; adjust the existing variables (function_ids iteration,
existing/state_get logic, compute_metrics call) to use the single drift result
map rather than invoking drift::handle per fid and fall back to None when a fid
has no entry.
eval/src/functions/ingest.rs (1)

21-26: Error message could be more descriptive.

The error message "missing {field}" doesn't indicate whether the field was absent or had wrong type. Consider distinguishing these cases.

Suggested improvement
 fn require_u64(payload: &Value, field: &str) -> Result<u64, IIIError> {
-    payload
-        .get(field)
-        .and_then(|v| v.as_u64())
-        .ok_or_else(|| IIIError::Handler(format!("missing {field}")))
+    match payload.get(field) {
+        Some(v) => v.as_u64().ok_or_else(|| IIIError::Handler(format!("{field} must be a non-negative integer"))),
+        None => Err(IIIError::Handler(format!("missing required field: {field}"))),
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/ingest.rs` around lines 21 - 26, The require_u64 helper
(fn require_u64) currently returns a generic "missing {field}" error; change it
to distinguish between a missing key and a present-but-wrong-type value by first
checking payload.get(field): if None return IIIError::Handler(format!("missing
field '{}'", field)), else if value.as_u64() is None return
IIIError::Handler(format!("field '{}' has wrong type: expected u64, found {}",
field, value.type_name() or value.to_string())), so callers can see whether the
key was absent or contained a non-u64 value; reference require_u64 and use
payload.get(field) and value.as_u64() to implement this branching.
eval/src/functions/metrics.rs (1)

48-53: Potential mismatch between total and durations.len().

total is set to spans.len() but avg_duration_ms divides by durations.len(). If some spans are missing duration_ms, these counts differ. This is intentional (average only over valid durations), but total_invocations in the output may not match the denominator used for average. Consider documenting this or using consistent counts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/metrics.rs` around lines 48 - 53, The code currently sets
total = spans.len() while avg_duration_ms divides by durations.len(), causing
total_invocations (total) to differ from the average's denominator if some spans
lack duration_ms; update the calculation to use a consistent count: compute a
new valid_count = durations.len() (or filter spans to count only those with
duration_ms), use valid_count for the average denominator and for
total_invocations (or explicitly document that total_invocations reflects all
spans while average is over valid durations). Locate the variables total, spans,
durations.len(), avg_duration_ms and total_invocations in metrics.rs and either
set total = durations.len() (or total = valid_count) or add a separate
total_valid_durations and use that for avg_duration_ms and output consistently.
eval/src/functions/analyze.rs (2)

256-268: Division guard relies on filter correctness.

Line 260 divides by s.invocations after filtering for s.error_count > 0. This is safe because error_count can only be incremented when invocations is also incremented (lines 159, 167), but the implicit coupling could be fragile. A defensive guard would be safer.

Optional defensive guard
     let errors: Vec<Value> = stats_map
         .iter()
-        .filter(|(_, s)| s.error_count > 0)
+        .filter(|(_, s)| s.error_count > 0 && s.invocations > 0)
         .map(|(fid, s)| {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/analyze.rs` around lines 256 - 268, The current mapping
that builds errors from stats_map computes error_rate by dividing s.error_count
by s.invocations (in the map that produces the `errors: Vec<Value>`), but this
relies on the invariant that invocations > 0; add a defensive guard inside that
closure used in the map (referencing `stats_map`, the closure parameters `(fid,
s)`, and `s.invocations`) so that if `s.invocations == 0` you set error_rate to
0.0 (or skip the entry) instead of performing the division, and then continue to
construct the JSON object with that guarded rate.

108-118: External trigger call should handle timeout gracefully.

The 10-second timeout is reasonable, but the error message "failed to fetch traces from engine" doesn't distinguish between timeout vs other failures. Consider enriching the error context.

Suggested improvement
         .await
         .map_err(|e| {
-            IIIError::Handler(format!("failed to fetch traces from engine: {e}"))
+            IIIError::Handler(format!("failed to fetch traces from engine::traces::list: {e}"))
         })?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/analyze.rs` around lines 108 - 118, The error mapping for
the external call iii.trigger(TriggerRequest { timeout_ms: Some(10_000), ... })
should distinguish timeouts from other failures; update the map_err closure that
produces IIIError::Handler for traces_response to inspect the returned error
(e.g., check for an is_timeout() method or look for "timeout" in e.to_string())
and return a clearer message like "timed out fetching traces from engine" for
timeout cases and keep the original "failed to fetch traces from engine: {e}"
for other errors so callers can tell timeout vs other failures.
eval/src/functions/mod.rs (1)

74-93: Index update logic could be moved into ingest::handle.

The function index update (checking if fid exists, appending if not) is performed in the registration wrapper rather than in ingest::handle. This splits related logic across two locations. Consider moving this into ingest::handle for better cohesion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/mod.rs` around lines 74 - 93, The registration wrapper in
mod.rs currently performs function index management (checking fid, fetching via
state::state_get, deserializing, pushing fid, and persisting with
state::state_set using ingest::SCOPE_INDEX and ingest::INDEX_KEY); move this
entire index-update block into ingest::handle so related ingest/register logic
lives together—remove the index update from the registration wrapper and call or
invoke the updated ingest::handle path that performs: retrieve index with
state::state_get, safely deserialize to Vec<String>, check contains(&fid), push
fid, and persist with state::state_set, preserving the tracing::warn on
deserialize or state_set errors. Ensure you reference the same symbols (fid,
state::state_get, state::state_set, ingest::SCOPE_INDEX, ingest::INDEX_KEY,
ingest::handle) and keep behavior (no-op if fid empty) unchanged.
eval/src/functions/score.rs (1)

7-7: Consider accepting EvalConfig for configurable thresholds.

The handle function uses hardcoded thresholds (0.95, 5000ms, 0.80, 10000ms) while drift::handle takes &EvalConfig for its configurable drift_threshold. For consistency and operational flexibility, the scoring thresholds should also be configurable.

Suggested signature change
-pub async fn handle(iii: &Arc<III>, _payload: Value) -> Result<Value, IIIError> {
+pub async fn handle(iii: &Arc<III>, config: &EvalConfig, _payload: Value) -> Result<Value, IIIError> {
+    let success_threshold = config.score_success_threshold.unwrap_or(0.95);
+    let p99_threshold_ms = config.score_p99_threshold_ms.unwrap_or(5000);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/score.rs` at line 7, The handle function in
eval/src/functions/score.rs currently hardcodes thresholds (0.95, 5000ms, 0.80,
10000ms); change its signature to accept an &EvalConfig (or a reference to it)
and replace those literal values with the corresponding fields from EvalConfig
(e.g., drift_threshold and timeout/threshold fields), and pass that config into
drift::handle (which already expects &EvalConfig); update any callers of
score::handle to supply the EvalConfig reference so scoring behavior becomes
configurable.
eval/SPEC.md (1)

13-19: Add language identifiers to fenced code blocks.

Static analysis flagged multiple code blocks without language specifiers. Add appropriate identifiers for better rendering and syntax highlighting.

Examples
-```
+```text
 Your Workers → OTel spans → PubSub topic "telemetry.spans" → eval::ingest
-```
+```json
 Input:  {
   function_id: string,

Also applies to: 25-29, 35-48, 54-61, 67-76, 86-96, 102-106, 111-119, 125-133

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/SPEC.md` around lines 13 - 19, Several fenced code blocks in SPEC.md
(e.g., the diagram block starting "Your Workers → OTel spans → PubSub topic
\"telemetry.spans\" → eval::ingest" and the JSON-like block starting "Input:  {
function_id: string,") are missing language identifiers; update each
triple-backtick fence to include an appropriate language label (e.g., ```text
for plain diagrams, ```json for JSON examples, ```yaml for YAML snippets) for
all affected blocks (also apply same change to the ranges noted: 25-29, 35-48,
54-61, 67-76, 86-96, 102-106, 111-119, 125-133) so that syntax highlighting and
rendering are correct.
eval/src/functions/drift.rs (1)

53-67: Relative threshold may be inappropriate for success_rate.

A 15% relative change in success_rate means:

  • 100% → 85% triggers drift (reasonable)
  • 90% → 76.5% triggers drift (reasonable)
  • 50% → 42.5% triggers drift (but 50% success rate is already critical)

Consider using absolute thresholds for success_rate (e.g., any drop > 5 percentage points) while keeping relative thresholds for latency metrics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/drift.rs` around lines 53 - 67, In the loop over
dimensions in drift.rs (the dimensions array and code computing baseline_v,
current_v and delta_pct), special-case the "success_rate" dimension: instead of
computing delta_pct as a relative change using baseline_v (the existing
delta_pct logic), compute an absolute change as abs(current_v - baseline_v) and
compare that to an absolute threshold (e.g., 0.05 for 5 percentage points); keep
the current relative delta_pct calculation for the latency metrics ("p50_ms",
"p95_ms", "p99_ms", "avg_duration_ms"). Ensure you reference the existing
symbols baseline_val, baseline_v, current, current_v, and delta_pct when adding
this branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@eval/README.md`:
- Around line 40-46: The fenced code block that begins with "Options:" is
missing a language tag; update the opening fence from ``` to ```text so the
block is marked (e.g., change the code fence before the "Options:" block to
```text) to satisfy MD040; leave the block contents and closing fence unchanged.

In `@eval/SPEC.md`:
- Line 31: The spec lists 6 functions but the implementation registers an extra
function eval::analyze_traces; add documentation for analyze_traces to
eval/SPEC.md (immediately after the eval::report section) using the suggested
block: specify Input: {limit?: integer, function_filter?: string}, Output: {
summary: {total_spans, unique_functions, time_range, error_rate},
slowest_functions: [...], most_active: [...], errors: [...], insights: [string]
} and a short description: "Reads OTel traces from the engine, aggregates
per-function stats, and generates insights about error rates and slow
functions." Ensure the new entry matches the formatting of the other function
docs so the total function count is consistent.
- Around line 25-29: Update the spec entry to match the implementation: replace
the line that documents eval:function_index with the implemented scope and key
by using the SCOPE_INDEX and INDEX_KEY names — i.e., document the state key as
eval:index:function_list and the description as "list of all tracked function
IDs" so the spec aligns with the constants SCOPE_INDEX and INDEX_KEY used in
ingest.rs.

In `@eval/src/config.rs`:
- Around line 4-17: The EvalConfig struct allows unknown/typoed config keys to
be ignored; add serde's deny_unknown_fields attribute to EvalConfig so unknown
YAML keys cause deserialization to fail fast. Modify the struct annotation for
EvalConfig to include #[serde(deny_unknown_fields)] (in addition to the existing
derives) so deserializing into EvalConfig will reject unexpected fields and
surface configuration mistakes.

In `@eval/src/functions/baseline.rs`:
- Around line 13-18: The code currently collapses all state_get failures into
json!(null) which hides backend/read errors; change the state read to explicitly
handle the Result from state_get (do not use unwrap_or(json!(null))). Match on
state_get(...).await: on Err(e) propagate or return the error (or log and return
a failure response) so callers know the read failed; on Ok(existing) then check
if existing.is_array() and parse the array into spans (using
serde_json::from_value on existing) and treat Null or non-array as empty Vec.
Update handling around the variables state_get, SCOPE_SPANS, function_id,
existing, and spans so read errors are not converted into "no span data".

In `@eval/src/functions/score.rs`:
- Around line 56-74: Scoring uses hardcoded absolute thresholds (success_rate <
0.95, p99 > 5000) which can conflict with the relative drift logic (15%
default); change the scoring in the function that adjusts fn_score (the block
referencing fn_score, success_rate, p99, and issues.push) to use configurable
thresholds (e.g., success_rate_threshold and p99_threshold) or compute
thresholds from the same relative baseline used by drift detection, then read
those values from the same config/params used by drift.rs (or surface them in
the shared config struct), and update issue payloads/tests/docs to reflect the
configurable/derived thresholds so scoring and drift detection align or
explicitly document the intended difference between absolute health vs relative
drift.

In `@eval/src/main.rs`:
- Around line 75-103: The trigger registration code calls
iii_arc.register_trigger for three critical triggers (cron -> function_id
"eval::drift", subscribe -> "eval::ingest", and http -> "eval::analyze_traces")
but only logs warnings on failure; change this to fail fast by treating
registration errors as fatal: propagate the Err out of main (or return an
Err/exit process) instead of continuing, e.g., replace the Err branches that do
tracing::warn!(...) with returning the error (or calling
anyhow::bail!/process::exit) and log at error level with context including the
register_trigger error and which RegisterTriggerInput failed (reference the
register_trigger calls and the RegisterTriggerInput structs/cront_expression) so
startup aborts when any of these critical triggers cannot be registered.
- Around line 62-71: register_worker() may fail to connect and its error is not
handled; change the code so you capture the Result from register_worker (instead
of unwrapping or ignoring errors), propagate or map the error into main()'s
Result with a clear message (e.g., using ? or map_err/anyhow::Context), only
create iii_arc and call functions::register_all(&iii_arc, &config) after a
successful register_worker return, and return an Err from main() with a
descriptive error when the connection to the III engine cannot be established.

---

Nitpick comments:
In `@eval/SPEC.md`:
- Around line 13-19: Several fenced code blocks in SPEC.md (e.g., the diagram
block starting "Your Workers → OTel spans → PubSub topic \"telemetry.spans\" →
eval::ingest" and the JSON-like block starting "Input:  { function_id: string,")
are missing language identifiers; update each triple-backtick fence to include
an appropriate language label (e.g., ```text for plain diagrams, ```json for
JSON examples, ```yaml for YAML snippets) for all affected blocks (also apply
same change to the ranges noted: 25-29, 35-48, 54-61, 67-76, 86-96, 102-106,
111-119, 125-133) so that syntax highlighting and rendering are correct.

In `@eval/src/config.rs`:
- Around line 51-54: After deserializing into EvalConfig in load_config,
validate semantic constraints before returning: check that
EvalConfig.drift_threshold is within 0.0..=1.0 and that
EvalConfig.max_spans_per_function is > 0 (and add any other domain rules
required by EvalConfig); if any check fails return an Err with a clear message
(e.g., using anyhow::bail or Result::Err with a descriptive string) instead of
Ok(config). Ensure the validation lives in load_config immediately after
serde_yaml::from_str so callers never receive an out-of-range EvalConfig.

In `@eval/src/functions/analyze.rs`:
- Around line 256-268: The current mapping that builds errors from stats_map
computes error_rate by dividing s.error_count by s.invocations (in the map that
produces the `errors: Vec<Value>`), but this relies on the invariant that
invocations > 0; add a defensive guard inside that closure used in the map
(referencing `stats_map`, the closure parameters `(fid, s)`, and
`s.invocations`) so that if `s.invocations == 0` you set error_rate to 0.0 (or
skip the entry) instead of performing the division, and then continue to
construct the JSON object with that guarded rate.
- Around line 108-118: The error mapping for the external call
iii.trigger(TriggerRequest { timeout_ms: Some(10_000), ... }) should distinguish
timeouts from other failures; update the map_err closure that produces
IIIError::Handler for traces_response to inspect the returned error (e.g., check
for an is_timeout() method or look for "timeout" in e.to_string()) and return a
clearer message like "timed out fetching traces from engine" for timeout cases
and keep the original "failed to fetch traces from engine: {e}" for other errors
so callers can tell timeout vs other failures.

In `@eval/src/functions/drift.rs`:
- Around line 53-67: In the loop over dimensions in drift.rs (the dimensions
array and code computing baseline_v, current_v and delta_pct), special-case the
"success_rate" dimension: instead of computing delta_pct as a relative change
using baseline_v (the existing delta_pct logic), compute an absolute change as
abs(current_v - baseline_v) and compare that to an absolute threshold (e.g.,
0.05 for 5 percentage points); keep the current relative delta_pct calculation
for the latency metrics ("p50_ms", "p95_ms", "p99_ms", "avg_duration_ms").
Ensure you reference the existing symbols baseline_val, baseline_v, current,
current_v, and delta_pct when adding this branch.

In `@eval/src/functions/ingest.rs`:
- Around line 21-26: The require_u64 helper (fn require_u64) currently returns a
generic "missing {field}" error; change it to distinguish between a missing key
and a present-but-wrong-type value by first checking payload.get(field): if None
return IIIError::Handler(format!("missing field '{}'", field)), else if
value.as_u64() is None return IIIError::Handler(format!("field '{}' has wrong
type: expected u64, found {}", field, value.type_name() or value.to_string())),
so callers can see whether the key was absent or contained a non-u64 value;
reference require_u64 and use payload.get(field) and value.as_u64() to implement
this branching.

In `@eval/src/functions/metrics.rs`:
- Around line 48-53: The code currently sets total = spans.len() while
avg_duration_ms divides by durations.len(), causing total_invocations (total) to
differ from the average's denominator if some spans lack duration_ms; update the
calculation to use a consistent count: compute a new valid_count =
durations.len() (or filter spans to count only those with duration_ms), use
valid_count for the average denominator and for total_invocations (or explicitly
document that total_invocations reflects all spans while average is over valid
durations). Locate the variables total, spans, durations.len(), avg_duration_ms
and total_invocations in metrics.rs and either set total = durations.len() (or
total = valid_count) or add a separate total_valid_durations and use that for
avg_duration_ms and output consistently.

In `@eval/src/functions/mod.rs`:
- Around line 74-93: The registration wrapper in mod.rs currently performs
function index management (checking fid, fetching via state::state_get,
deserializing, pushing fid, and persisting with state::state_set using
ingest::SCOPE_INDEX and ingest::INDEX_KEY); move this entire index-update block
into ingest::handle so related ingest/register logic lives together—remove the
index update from the registration wrapper and call or invoke the updated
ingest::handle path that performs: retrieve index with state::state_get, safely
deserialize to Vec<String>, check contains(&fid), push fid, and persist with
state::state_set, preserving the tracing::warn on deserialize or state_set
errors. Ensure you reference the same symbols (fid, state::state_get,
state::state_set, ingest::SCOPE_INDEX, ingest::INDEX_KEY, ingest::handle) and
keep behavior (no-op if fid empty) unchanged.

In `@eval/src/functions/report.rs`:
- Around line 35-42: Replace the silent .ok() drop of errors when calling
crate::functions::drift::handle so drift failures are observable: change the
handling around the drift_result (the block using has_baseline, drift_payload,
and the call to crate::functions::drift::handle(iii, config, drift_payload)) to
capture the Result, log the error via the existing logger or attach an error
string to the report (e.g., a drift_error field) instead of returning None, and
ensure success still yields the drift payload; this preserves the existing flow
but surfaces errors for observability.
- Around line 18-50: The per-function loop currently calls
crate::functions::drift::handle(...) for each fid; instead, call
crate::functions::drift::handle once outside the loop with a payload that omits
"function_id" to get drift results for all functions, then inside the loop
lookup the drift entry for each fid (e.g., map by function_id) when building
function_reports; adjust the existing variables (function_ids iteration,
existing/state_get logic, compute_metrics call) to use the single drift result
map rather than invoking drift::handle per fid and fall back to None when a fid
has no entry.

In `@eval/src/functions/score.rs`:
- Line 7: The handle function in eval/src/functions/score.rs currently hardcodes
thresholds (0.95, 5000ms, 0.80, 10000ms); change its signature to accept an
&EvalConfig (or a reference to it) and replace those literal values with the
corresponding fields from EvalConfig (e.g., drift_threshold and
timeout/threshold fields), and pass that config into drift::handle (which
already expects &EvalConfig); update any callers of score::handle to supply the
EvalConfig reference so scoring behavior becomes configurable.

In `@eval/src/manifest.rs`:
- Around line 17-23: The manifest hardcodes default_config JSON that duplicates
EvalConfig::default(), causing drift; replace the hardcoded serde_json::json!
block with a conversion from EvalConfig::default() (e.g., serialize or convert
EvalConfig::default() into a serde_json::Value) so default_config is derived
from EvalConfig::default() instead of duplicated; update the code that
constructs default_config (the variable named default_config in this diff) to
call EvalConfig::default() and serialize/convert it into the same JSON/Value
shape used elsewhere.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 82ebb290-77da-43ab-a529-29e62ee9cc33

📥 Commits

Reviewing files that changed from the base of the PR and between 606a4d4 and 4068e9c.

📒 Files selected for processing (17)
  • eval/Cargo.toml
  • eval/README.md
  • eval/SPEC.md
  • eval/build.rs
  • eval/config.yaml
  • eval/src/config.rs
  • eval/src/functions/analyze.rs
  • eval/src/functions/baseline.rs
  • eval/src/functions/drift.rs
  • eval/src/functions/ingest.rs
  • eval/src/functions/metrics.rs
  • eval/src/functions/mod.rs
  • eval/src/functions/report.rs
  • eval/src/functions/score.rs
  • eval/src/functions/state.rs
  • eval/src/main.rs
  • eval/src/manifest.rs

Comment thread eval/README.md Outdated
Comment thread eval/SPEC.md Outdated
Comment on lines +25 to +29
```
eval:spans:{function_id} — array of span objects (capped at max_spans_per_function)
eval:baselines:{function_id} — baseline metrics snapshot for drift comparison
eval:function_index — list of all tracked function IDs
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

State scope key mismatch with implementation.

The spec says eval:function_index but the code (ingest.rs:10-11) uses:

  • SCOPE_INDEX = "eval:index"
  • INDEX_KEY = "function_list"

Update the spec to match the implementation.

Suggested fix

eval:spans:{function_id} — array of span objects (capped at max_spans_per_function)
eval:baselines:{function_id} — baseline metrics snapshot for drift comparison
-eval:function_index — list of all tracked function IDs
+eval:index:function_list — list of all tracked function IDs

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 25-25: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/SPEC.md` around lines 25 - 29, Update the spec entry to match the
implementation: replace the line that documents eval:function_index with the
implemented scope and key by using the SCOPE_INDEX and INDEX_KEY names — i.e.,
document the state key as eval:index:function_list and the description as "list
of all tracked function IDs" so the spec aligns with the constants SCOPE_INDEX
and INDEX_KEY used in ingest.rs.

Comment thread eval/SPEC.md Outdated
Comment thread eval/src/config.rs
Comment thread eval/src/functions/baseline.rs Outdated
Comment on lines +56 to +74
if success_rate < 0.95 {
let penalty = (0.95 - success_rate) * 200.0;
fn_score -= penalty;
issues.push(json!({
"function_id": fid,
"issue": "low_success_rate",
"value": success_rate,
}));
}

if p99 > 5000 {
let penalty = ((p99 as f64 - 5000.0) / 1000.0).min(30.0);
fn_score -= penalty;
issues.push(json!({
"function_id": fid,
"issue": "high_p99_latency",
"value_ms": p99,
}));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded scoring thresholds may conflict with drift detection logic.

The scoring penalties use absolute thresholds (success_rate < 0.95, p99 > 5000ms), while drift detection (per context snippet from drift.rs:53-79) uses a configurable relative threshold (default 15%). This can produce contradictory signals:

  • A function with 94% success rate triggers a score penalty but may not trigger drift if baseline was 93%.
  • A function degrading from 99% to 86% (13% relative change) won't trigger drift but will trigger scoring issues.

Consider aligning the thresholds or documenting the intentional difference between "absolute health" (score) vs "relative change" (drift).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/score.rs` around lines 56 - 74, Scoring uses hardcoded
absolute thresholds (success_rate < 0.95, p99 > 5000) which can conflict with
the relative drift logic (15% default); change the scoring in the function that
adjusts fn_score (the block referencing fn_score, success_rate, p99, and
issues.push) to use configurable thresholds (e.g., success_rate_threshold and
p99_threshold) or compute thresholds from the same relative baseline used by
drift detection, then read those values from the same config/params used by
drift.rs (or surface them in the shared config struct), and update issue
payloads/tests/docs to reflect the configurable/derived thresholds so scoring
and drift detection align or explicitly document the intended difference between
absolute health vs relative drift.

Comment thread eval/src/main.rs
Comment on lines +62 to +71
let iii = register_worker(
&cli.url,
InitOptions {
otel: Some(OtelConfig::default()),
..Default::default()
},
);

let iii_arc = Arc::new(iii);
functions::register_all(&iii_arc, &config);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the return type and error handling of register_worker in iii_sdk
# to verify if it can fail and how errors are propagated

# Search for register_worker function definition or usage patterns
rg -n "register_worker" --type rust -C 5

# Look for Result types in the SDK imports or worker patterns
ast-grep --pattern 'fn register_worker($$$) -> $RET { $$$ }'

Repository: iii-hq/workers

Length of output: 1869


🏁 Script executed:

find . -name "*.rs" -type f | xargs grep -l "pub fn register_worker\|pub async fn register_worker" | head -20

Repository: iii-hq/workers

Length of output: 40


🏁 Script executed:

cat Cargo.toml | grep -A 5 "\[dependencies\]" | head -20

Repository: iii-hq/workers

Length of output: 100


🏁 Script executed:

# Check if iii_sdk is local or external
ls -la | grep -E "iii_sdk|Cargo"

Repository: iii-hq/workers

Length of output: 40


🏁 Script executed:

# Look at other main.rs files to see if there's a pattern for error handling
fd "main\.rs" --type f | xargs rg -l "register_worker" | head -10

Repository: iii-hq/workers

Length of output: 99


🏁 Script executed:

find . -name "Cargo.toml" -type f | head -20

Repository: iii-hq/workers

Length of output: 101


🏁 Script executed:

# Check for iii_sdk in git or as a path dependency
rg "iii_sdk" --type toml -C 3

Repository: iii-hq/workers

Length of output: 40


🏁 Script executed:

# Look for documentation or comments about register_worker behavior
rg "register_worker" --type rust -C 10 | head -100

Repository: iii-hq/workers

Length of output: 3243


🏁 Script executed:

cat eval/Cargo.toml

Repository: iii-hq/workers

Length of output: 641


🏁 Script executed:

cat image-resize/Cargo.toml

Repository: iii-hq/workers

Length of output: 709


🏁 Script executed:

# Check if main() function has special error handling or try-catch
rg "fn main|async fn main" --type rust -A 20 eval/src/main.rs | head -40

Repository: iii-hq/workers

Length of output: 742


🏁 Script executed:

# Check if there are any panic catches or error recovery patterns
rg "panic|catch_unwind|std::panic" --type rust

Repository: iii-hq/workers

Length of output: 150


🏁 Script executed:

# Search for any comments or documentation about register_worker behavior
rg "register_worker" --type rust --type md --type txt -B 5 -A 5

Repository: iii-hq/workers

Length of output: 1771


🏁 Script executed:

# Look for test files that might show register_worker behavior
find . -name "*test*" -o -name "*spec*" | grep -E "\.rs$"

Repository: iii-hq/workers

Length of output: 40


🏁 Script executed:

# Check how 'iii' is used after register_worker to infer its type/behavior
rg "let iii = register_worker" --type rust -A 20

Repository: iii-hq/workers

Length of output: 2408


🏁 Script executed:

# Look for any README or documentation files
find . -name "README*" -o -name "*.md" | head -10

Repository: iii-hq/workers

Length of output: 134


🏁 Script executed:

cat README.md

Repository: iii-hq/workers

Length of output: 1591


🏁 Script executed:

cat eval/README.md

Repository: iii-hq/workers

Length of output: 1933


Add error handling for register_worker connection failures.

The register_worker call connects to the III engine via WebSocket synchronously, but the connection error is not exposed for handling. If the engine is unreachable at startup, the behavior is unclear (likely panic or hang). Since main() returns Result<()>, consider wrapping the connection attempt in error handling to enable graceful startup failure with a clear error message when the engine is unavailable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/main.rs` around lines 62 - 71, register_worker() may fail to connect
and its error is not handled; change the code so you capture the Result from
register_worker (instead of unwrapping or ignoring errors), propagate or map the
error into main()'s Result with a clear message (e.g., using ? or
map_err/anyhow::Context), only create iii_arc and call
functions::register_all(&iii_arc, &config) after a successful register_worker
return, and return an Err from main() with a descriptive error when the
connection to the III engine cannot be established.

Comment thread eval/src/main.rs Outdated
- Drop features = ["otel"] (OTel is now always-on in 0.11.0)
- Add metadata: None to RegisterTriggerInput constructors (new required field)
- 19 tests pass

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

♻️ Duplicate comments (5)
eval/src/functions/baseline.rs (1)

13-18: ⚠️ Potential issue | 🟠 Major

Do not collapse state read failures into “no span data.”

Line 13 still turns all state_get errors into null, so backend/read failures become a misleading "no span data" response instead of surfacing the real failure.

Proposed fix
-    let existing = state_get(iii, crate::functions::ingest::SCOPE_SPANS, function_id).await.unwrap_or(json!(null));
+    let existing = state_get(iii, crate::functions::ingest::SCOPE_SPANS, function_id)
+        .await
+        .map_err(|e| IIIError::Handler(format!("failed to read spans for {function_id}: {e}")))?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/baseline.rs` around lines 13 - 18, The code currently
masks state_get errors by using .await.unwrap_or(json!(null)), making
backend/read failures appear as "no span data"; instead change the state read to
handle the Result explicitly: call state_get(...).await and match or use the ?
operator to propagate the error (referencing state_get,
crate::functions::ingest::SCOPE_SPANS, and function_id), then only when
Ok(value) check if value.is_array() and deserialize into spans (Vec<Value>) with
serde_json::from_value(...).map_err(...) or unwrap_or_default for
deserialization failures; do not convert Err from state_get into json!(null) —
propagate or return the error so real failures surface.
eval/SPEC.md (2)

31-121: ⚠️ Potential issue | 🟡 Minor

Document the seventh registered function.

The PR and README describe 7 functions, but the spec still says 6 and omits eval::analyze_traces.

Proposed addition
-## Functions (6)
+## Functions (7)
 Combines metrics + drift + score into a single comprehensive report across all tracked functions.
+
+### eval::analyze_traces
+
+```text
+Input:  {limit?: integer, function_filter?: string}
+Output: {
+  summary: {total_spans, unique_functions, time_range, error_rate},
+  slowest_functions: [...],
+  most_active: [...],
+  errors: [...],
+  insights: [string]
+}
+```
+
+Reads OTel traces from the engine, aggregates per-function stats, and generates insights about error rates and slow functions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/SPEC.md` around lines 31 - 121, The spec currently lists six functions
but the PR/README register seven; add documentation for the missing
eval::analyze_traces function to the Functions section (update the header count
to 7) by inserting an entry named eval::analyze_traces that documents its Input
({limit?: integer, function_filter?: string}), Output (summary: {total_spans,
unique_functions, time_range, error_rate}, slowest_functions, most_active,
errors, insights), and a one-line description ("Reads OTel traces from the
engine, aggregates per-function stats, and generates insights about error rates
and slow functions"); place this new entry alongside the other function blocks
(e.g., after eval::report or where functions are enumerated) so the spec matches
the PR/README.

25-29: ⚠️ Potential issue | 🟡 Minor

Align the documented function index key with the implementation.

The spec still says eval:function_index, but the code stores the index at scope/key eval:index:function_list. Line 50 should use the same name as well.

Proposed fix
 eval:spans:{function_id}      — array of span objects (capped at max_spans_per_function)
 eval:baselines:{function_id}  — baseline metrics snapshot for drift comparison
-eval:function_index            — list of all tracked function IDs
+eval:index:function_list       — list of all tracked function IDs
-Appends a span to `eval:spans:{function_id}`. Trims the list to `max_spans_per_function` (oldest evicted first). Maintains the `eval:function_index` for discovery by other functions.
+Appends a span to `eval:spans:{function_id}`. Trims the list to `max_spans_per_function` (oldest evicted first). Maintains `eval:index:function_list` for discovery by other functions.

Also applies to: 50-50

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/SPEC.md` around lines 25 - 29, The spec documents the function index
under the key eval:function_index but the implementation uses
eval:index:function_list; update the documented key in the SPEC.md block to
match the implementation by replacing eval:function_index with
eval:index:function_list so the spec and code use the same unique key name.
eval/src/config.rs (1)

4-17: ⚠️ Potential issue | 🟠 Major

Reject unknown config keys.

A typo in config.yaml is currently ignored and the default value is silently used, which can make production drift/retention settings look applied when they are not. This is the same unresolved config-safety issue previously raised.

Proposed fix
-#[derive(Deserialize, Debug, Clone)]
+#[derive(Deserialize, Debug, Clone)]
+#[serde(deny_unknown_fields)]
 pub struct EvalConfig {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eval/src/config.rs` around lines 4 - 17, Add strict deserialization to
EvalConfig so unknown keys cause an error: annotate the EvalConfig struct (the
type named EvalConfig that derives Deserialize) with serde(deny_unknown_fields)
to reject typos in config.yaml instead of silently falling back to defaults;
ensure the Deserialize derive remains and run/adjust any config-loading tests to
expect failures on unknown keys.
eval/README.md (1)

42-48: ⚠️ Potential issue | 🟡 Minor

Add a language tag to the options fence.

The options block is still missing a fenced-code language tag.

Proposed fix
-```
+```text
 Options:
   --config <PATH>    Path to config.yaml [default: ./config.yaml]
   --url <URL>        WebSocket URL of the iii engine [default: ws://127.0.0.1:49134]
   --manifest         Output module manifest as JSON and exit
   -h, --help         Print help
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @eval/README.md around lines 42 - 48, The fenced code block showing the CLI
Options is missing a language tag; update the block that starts with the line
"Options:" so the opening fence is "text" (or another appropriate language identifier) instead of just "" to ensure proper syntax highlighting for the
options listing in README.md.


</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>eval/src/functions/analyze.rs (2)</summary><blockquote>

`75-95`: **Derive `Default` instead of hand-rolling it.**

All fields already have trivial defaults (`0`, `0.0`, `None`); `#[derive(Default)]` removes ~10 lines of boilerplate without behavior change.

<details>
<summary>♻️ Proposed refactor</summary>

```diff
-struct FunctionStats {
-    invocations: u64,
-    total_duration_ms: f64,
-    error_count: u64,
-    last_error: Option<String>,
-    min_timestamp_ms: Option<i64>,
-    max_timestamp_ms: Option<i64>,
-}
-
-impl Default for FunctionStats {
-    fn default() -> Self {
-        FunctionStats {
-            invocations: 0,
-            total_duration_ms: 0.0,
-            error_count: 0,
-            last_error: None,
-            min_timestamp_ms: None,
-            max_timestamp_ms: None,
-        }
-    }
-}
+#[derive(Default)]
+struct FunctionStats {
+    invocations: u64,
+    total_duration_ms: f64,
+    error_count: u64,
+    last_error: Option<String>,
+    min_timestamp_ms: Option<i64>,
+    max_timestamp_ms: Option<i64>,
+}
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/analyze.rs` around lines 75 - 95, Replace the manual
Default implementation for FunctionStats with an auto-derived one: remove the
impl Default block and add #[derive(Default)] (alongside any existing derives)
to the FunctionStats struct declaration so invocations, total_duration_ms,
error_count, last_error, min_timestamp_ms, and max_timestamp_ms use their
trivial defaults automatically.
```

</details>

---

`55-73`: **Minor: integer-float timestamps and unparseable RFC3339 strings silently become `None`.**

Two edge cases on `extract_timestamp_ms`:

1. If `timestamp` is a JSON number serialized as a float (e.g., `1.7e12`), `as_i64()` at Line 61 returns `None` and the span contributes no timestamp. `as_f64().map(|f| f as i64)` would cover this.
2. If `timestamp` is a string but fails `parse_from_rfc3339`, the function falls through to the `start_time` checks — that's fine — but the parse error is swallowed with no log, so misformatted timestamps are invisible in ops. Consider a `tracing::debug!` on parse failure.

Non-blocking; flagging for robustness depending on the real span payload shape emitted by `engine::traces::list`.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@eval/src/functions/analyze.rs` around lines 55 - 73, In extract_timestamp_ms,
handle float-valued numeric timestamps and surface RFC3339 parse failures: when
checking "timestamp" and "start_time" also try as_f64().map(|f| f as i64) if
as_i64() returns None, and on any DateTime::parse_from_rfc3339 failure emit a
tracing::debug! (including the offending string and field name) so malformed
string timestamps aren't silently dropped; update the logic in
extract_timestamp_ms to attempt these additional conversions and add debug
logging around parse errors.
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @eval/README.md:

  • Around line 9-17: The function table in README.md is missing the registered
    trace-analysis endpoint; add a new table row for eval::analyze_traces so the
    table lists 7 functions like the worker does, e.g. include
    eval::analyze_traces in the Function ID column and a short Description such as
    "Analyze ingested traces/spans to aggregate latency distributions and detect
    anomalies" to match the registered API surface.

In @eval/src/config.rs:

  • Around line 51-54: load_config currently returns deserialized EvalConfig
    without validating operational fields; add validation inside load_config after
    deserializing (the EvalConfig produced by serde_yaml::from_str) to fail-fast on
    invalid values: ensure max_spans_per_function > 0 and that drift_threshold is
    finite and non-negative (or within acceptable bounds your app expects),
    returning an Err with a clear message if checks fail; update load_config to
    perform these checks on the EvalConfig instance and return Ok(config) only when
    validations pass.

In @eval/src/functions/analyze.rs:

  • Around line 165-172: The current logic only updates stats.last_error when
    extract_error_message(span) returns Some, which leaves a stale message when a
    later error span has no message; modify the error-handling block in analyze.rs
    (around extract_is_error, extract_error_message, stats.last_error,
    stats.error_count, total_errors) so that when is_err is true you always set
    stats.last_error = extract_error_message(span) (allowing None to clear the
    previous value) while still incrementing stats.error_count and total_errors;
    alternatively, if you prefer "last non-empty" semantics, document that behavior
    clearly and keep the current code.

In @eval/src/functions/drift.rs:

  • Around line 12-40: The state reads (calls to state_get used for index_val,
    baseline_val, and existing/spans with symbols state_get,
    crate::functions::ingest::SCOPE_INDEX/INDEX_KEY, SCOPE_BASELINES, SCOPE_SPANS)
    swallow errors via unwrap_or(json!(null)) which turns backend failures into
    misleading "no_baseline"/empty results; change these calls to propagate the
    underlying error instead of mapping to null (e.g., use the ? operator or
    explicitly match Err and return an Err) and update the surrounding function
    signature to return a Result so callers can detect and handle state read
    failures rather than treating them as harmless “no drift” conditions.

In @eval/src/functions/ingest.rs:

  • Around line 57-73: The current code swallows state_get errors (state_get(iii,
    SCOPE_SPANS, &function_id).await.unwrap_or(json!(null))) which can cause
    accidental overwrite of existing spans; change the logic to propagate the error
    from state_get (return or ? the Err) instead of treating it as null, then
    deserialize when Ok. Also address the TOCTOU race in the get→modify→set sequence
    by either documenting the limitation or serializing concurrent ingests for the
    same function_id (e.g., a per-function mutex keyed by function_id around the
    read/modify/write) so spans are not lost; keep references to state_get,
    state_set, SCOPE_SPANS, function_id, spans, and config.max_spans_per_function
    when applying the fix.

Duplicate comments:
In @eval/README.md:

  • Around line 42-48: The fenced code block showing the CLI Options is missing a
    language tag; update the block that starts with the line "Options:" so the
    opening fence is "text" (or another appropriate language identifier) instead of just "" to ensure proper syntax highlighting for the options listing in
    README.md.

In @eval/SPEC.md:

  • Around line 31-121: The spec currently lists six functions but the PR/README
    register seven; add documentation for the missing eval::analyze_traces function
    to the Functions section (update the header count to 7) by inserting an entry
    named eval::analyze_traces that documents its Input ({limit?: integer,
    function_filter?: string}), Output (summary: {total_spans, unique_functions,
    time_range, error_rate}, slowest_functions, most_active, errors, insights), and
    a one-line description ("Reads OTel traces from the engine, aggregates
    per-function stats, and generates insights about error rates and slow
    functions"); place this new entry alongside the other function blocks (e.g.,
    after eval::report or where functions are enumerated) so the spec matches the
    PR/README.
  • Around line 25-29: The spec documents the function index under the key
    eval:function_index but the implementation uses eval:index:function_list; update
    the documented key in the SPEC.md block to match the implementation by replacing
    eval:function_index with eval:index:function_list so the spec and code use the
    same unique key name.

In @eval/src/config.rs:

  • Around line 4-17: Add strict deserialization to EvalConfig so unknown keys
    cause an error: annotate the EvalConfig struct (the type named EvalConfig that
    derives Deserialize) with serde(deny_unknown_fields) to reject typos in
    config.yaml instead of silently falling back to defaults; ensure the Deserialize
    derive remains and run/adjust any config-loading tests to expect failures on
    unknown keys.

In @eval/src/functions/baseline.rs:

  • Around line 13-18: The code currently masks state_get errors by using
    .await.unwrap_or(json!(null)), making backend/read failures appear as "no span
    data"; instead change the state read to handle the Result explicitly: call
    state_get(...).await and match or use the ? operator to propagate the error
    (referencing state_get, crate::functions::ingest::SCOPE_SPANS, and function_id),
    then only when Ok(value) check if value.is_array() and deserialize into spans
    (Vec) with serde_json::from_value(...).map_err(...) or unwrap_or_default
    for deserialization failures; do not convert Err from state_get into json!(null)
    — propagate or return the error so real failures surface.

Nitpick comments:
In @eval/src/functions/analyze.rs:

  • Around line 75-95: Replace the manual Default implementation for FunctionStats
    with an auto-derived one: remove the impl Default block and add
    #[derive(Default)] (alongside any existing derives) to the FunctionStats struct
    declaration so invocations, total_duration_ms, error_count, last_error,
    min_timestamp_ms, and max_timestamp_ms use their trivial defaults automatically.
  • Around line 55-73: In extract_timestamp_ms, handle float-valued numeric
    timestamps and surface RFC3339 parse failures: when checking "timestamp" and
    "start_time" also try as_f64().map(|f| f as i64) if as_i64() returns None, and
    on any DateTime::parse_from_rfc3339 failure emit a tracing::debug! (including
    the offending string and field name) so malformed string timestamps aren't
    silently dropped; update the logic in extract_timestamp_ms to attempt these
    additional conversions and add debug logging around parse errors.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro

**Run ID**: `e451d94e-3ebb-4647-a994-c62a5f2deed3`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4068e9cfe7f8ef44f61bd55d8dabbadf21e66c6d and be300200a79e5fe485a1bbfbbdde11fedb5359f3.

</details>

<details>
<summary>📒 Files selected for processing (17)</summary>

* `eval/Cargo.toml`
* `eval/README.md`
* `eval/SPEC.md`
* `eval/build.rs`
* `eval/config.yaml`
* `eval/src/config.rs`
* `eval/src/functions/analyze.rs`
* `eval/src/functions/baseline.rs`
* `eval/src/functions/drift.rs`
* `eval/src/functions/ingest.rs`
* `eval/src/functions/metrics.rs`
* `eval/src/functions/mod.rs`
* `eval/src/functions/report.rs`
* `eval/src/functions/score.rs`
* `eval/src/functions/state.rs`
* `eval/src/main.rs`
* `eval/src/manifest.rs`

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* eval/config.yaml
* eval/Cargo.toml

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (8)</summary>

* eval/build.rs
* eval/src/functions/state.rs
* eval/src/manifest.rs
* eval/src/functions/score.rs
* eval/src/main.rs
* eval/src/functions/report.rs
* eval/src/functions/mod.rs
* eval/src/functions/metrics.rs

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread eval/README.md
Comment thread eval/src/config.rs
Comment thread eval/src/functions/analyze.rs
Comment thread eval/src/functions/drift.rs Outdated
Comment thread eval/src/functions/ingest.rs Outdated
@rohitg00

Copy link
Copy Markdown
Contributor Author

Heads up — main is being bumped to iii-sdk =0.11.3 in #33. When that lands, please rebase and bump this worker's pin (currently 0.11.0) to =0.11.3. Two 0.11.x deltas that may bite:

  • iii-sdk no longer exposes an otel cargo feature — OTel is always-on. Drop features = ["otel"] if you use it.
  • WorkerMetadata gained an isolation field. If you construct it as a struct literal, add ..Default::default() (or fill the field). iii-lsp hit this; see the fix in chore: bump iii-sdk to 0.11.3 across all workers #33.
  • register_function(msg, handler) (two-arg) is now register_function_with(msg, handler); register_function is single-arg via IntoFunctionRegistration. image-resize hit this; see chore: bump iii-sdk to 0.11.3 across all workers #33.

Release notes: https://github.com/iii-hq/iii/releases/tag/iii/v0.11.3

…doc alignment

- config.rs: deny_unknown_fields so typos in YAML fail loud; validate()
  after deserialize rejects zero retention_hours/max_spans/window,
  non-finite/negative drift_threshold, empty cron or model.
- ingest.rs: propagate state_get errors instead of swallowing them into
  'empty array' (which would then overwrite existing spans with only
  the new one). Document the get→modify→set TOCTOU — no CAS in engine,
  per-function serialization is the caller's job if needed.
- baseline.rs: propagate state_get errors rather than flattening them
  into 'no span data'; surface backend failures distinctly.
- drift.rs: propagate read failures on function index, baseline, and
  spans so drift checks can't look healthy while state is unavailable.
- analyze.rs: every error span overwrites stats.last_error, including
  the None case — previous logic kept stale messages from earlier spans
  and misreported them as 'last_error'.
- score.rs: add an explanatory comment on why the absolute thresholds
  differ from drift's relative threshold (ops SLO vs. regression).
- main.rs: aggregate trigger registration errors and abort startup if
  any trigger failed, instead of logging a warning and continuing with
  a half-wired worker.
- README: add eval::analyze_traces row; tag the fenced options block
  with a language (MD040).
- .gitignore: target/ + Cargo.lock per repo convention.
@rohitg00
rohitg00 merged commit bbf99e2 into main Apr 22, 2026
5 checks passed
@rohitg00
rohitg00 deleted the feat/eval branch April 22, 2026 23:20
@coderabbitai coderabbitai Bot mentioned this pull request Apr 23, 2026
2 tasks
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