Skip to content

Add KPI filters and detail enrichment for component feedback#700

Draft
jvang-rh wants to merge 1 commit into
RedHatProductSecurity:mainfrom
jvang-rh:feature/kpi-component-filters
Draft

Add KPI filters and detail enrichment for component feedback#700
jvang-rh wants to merge 1 commit into
RedHatProductSecurity:mainfrom
jvang-rh:feature/kpi-component-filters

Conversation

@jvang-rh

@jvang-rh jvang-rh commented Jun 30, 2026

Copy link
Copy Markdown

What

Extend GET /api/v1/analysis/kpi/cve with component feedback filters and optional detail enrichment.

Why

Analysts need to query KPI data by CVE, suggested component, and multi-component suggestions, and inspect component diffs (accepted/rejected/added) without post-processing raw feedback logs.

How

  • Add query params: cve_id, source_component, multiple_source_components, detail
  • Map source_component feature alias to both source_component and suggest-affected-components log entries
  • Return optional KPIEntry fields when detail=true (suggested_components, accepted_components, rejected_suggestions, added_components, etc.)
  • Set response_model_exclude_none=True so detail=false responses stay compact

How to Test

uv run pytest src/aegis_ai_web/tests/test_kpi.py -q

After staging deploy:

curl --negotiate -u : -f \
  'https://aegis.prodsec.redhat.com/api/v1/analysis/kpi/cve?feature=source_component&source_component=kernel&detail=true'

Related Tickets

Summary by Sourcery

Extend the CVE KPI endpoint to support component feedback filtering and optional detail fields for enriched analysis.

New Features:

  • Add component-specific KPI query parameters (cve_id, source_component, multiple_source_components, detail) to GET /api/v1/analysis/kpi/cve.
  • Expose optional CVE and component context fields on KPI entries when detail=true, including suggested, submitted, accepted, rejected, and added components.
  • Support source_component feature aliasing so KPI queries cover both source_component and suggest-affected-components feedback logs.

Enhancements:

  • Refactor KPI computation to normalize manual and programmatic feedback entries, apply filters, and compute acceptance metrics from a unified representation.
  • Ensure KPI API responses omit null fields via response_model_exclude_none for compact payloads when detail=false.

Documentation:

  • Document the new KPI query parameters and enriched entry fields in the FastAPI endpoint docs and CHANGELOG, including component feedback filtering behavior.

Tests:

  • Add unit and HTTP tests covering component alias resolution, component diffing, new KPI filters, detail enrichment behavior, and validation of CVE identifiers.

Extend GET /api/v1/analysis/kpi/cve with cve_id, source_component, and
multiple_source_components filters plus detail=true component diff fields.
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added new filters to KPI views so results can be narrowed by CVE, source component, or entries with multiple suggested components.
    • Added a detail mode that shows richer CVE and component breakdown information in KPI responses.
    • Improved API responses to omit empty fields and better document supported query options.
  • Bug Fixes

    • KPI totals now recalculate correctly after filters are applied.
    • Matching is more flexible for component-related KPI views, including alias handling.

Walkthrough

Adds cve_id, source_component, multiple_source_components, and detail query parameters to the /api/v1/analysis/kpi/cve endpoint. Refactors the KPI computation pipeline in kpi.py with normalization, filtering, and component-diffing helpers. Extends KPIEntry with optional detail fields. Adds comprehensive unit and HTTP tests.

Changes

KPI Filtering and Detail Mode

Layer / File(s) Summary
KPIEntry detail fields
src/aegis_ai_web/src/data_models.py
Adds optional cve_id, email, feedback_source, and component-list fields (suggested_components, submitted_components, accepted_components, rejected_suggestions, added_components) to KPIEntry, all defaulting to None and documented as populated only when detail=true.
KPI normalization, filtering, and detail pipeline
src/aegis_ai_web/src/endpoints/kpi.py
Introduces COMPONENT_FEATURE_KEYS, _resolve_feature_aliases, _parse_components, _component_diff, row normalization helpers, _matches_entry_filters, _collect_normalized_entries, _to_kpi_entry(detail), and updated _compute_kpi/get_cve_kpi accepting the new keyword-only filters.
FastAPI endpoint wiring
src/aegis_ai_web/src/main.py
Adds cve_id, source_component, multiple_source_components, and detail Query parameters to cve_kpi; sets response_model_exclude_none=True; passes new args to get_cve_kpi.
Tests and changelog
src/aegis_ai_web/tests/test_kpi.py, docs/CHANGELOG.md
Adds TestComponentKpiHelpers, TestComponentKpiFilters, and TestComponentKpiApi covering alias resolution, component diffing, all filter combinations, detail enrichment, and HTTP response shape; changelog documents the new filters and detail mode.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: KPI filtering and detail enrichment for component feedback.
Description check ✅ Passed The description covers the required What, Why, How, How to Test, and Related Tickets sections and aligns with the PR scope.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@sourcery-ai sourcery-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.

Hey - I've found 7 issues, and left some high level feedback:

  • When feature='all' is used together with source_component/multiple_source_components filters, non-component features are still included and filtered based on actual/suggested_value parsed as JSON lists; consider either restricting these filters to component features or explicitly excluding non-component features to avoid surprising results.
  • The component diff helpers currently sort accepted/rejected/added lists, which can lose the original ordering from the feedback logs; if analysts rely on the sequence of components, preserving the source order instead of sorting may be more appropriate.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- When `feature='all'` is used together with `source_component`/`multiple_source_components` filters, non-component features are still included and filtered based on `actual`/`suggested_value` parsed as JSON lists; consider either restricting these filters to component features or explicitly excluding non-component features to avoid surprising results.
- The component diff helpers currently sort accepted/rejected/added lists, which can lose the original ordering from the feedback logs; if analysts rely on the sequence of components, preserving the source order instead of sorting may be more appropriate.

## Individual Comments

### Comment 1
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1157-1166" />
<code_context>
+class TestComponentKpiHelpers:
</code_context>
<issue_to_address>
**suggestion (testing):** Add edge-case coverage for `_component_diff` (empty, whitespace, duplicates)

Please add tests for `_component_diff` where:
- `suggested` and/or `submitted` are empty lists
- entries are empty strings or whitespace only
- arrays contain duplicate component names (to verify expected handling/order)

These will clarify the intended semantics under noisy input and ensure the helper behaves correctly in common edge cases.

Suggested implementation:

```python
class TestComponentKpiHelpers:
    """Unit tests for component KPI helper functions."""

    def test_resolve_feature_aliases_for_source_component(self):
        from aegis_ai_web.src.endpoints.kpi import _resolve_feature_aliases

        aliases = _resolve_feature_aliases("source_component")
        assert aliases == {"source_component", "suggest-affected-components"}

    def test_resolve_feature_aliases_for_unrelated_feature(self):
        from aegis_ai_web.src.endpoints.kpi import _resolve_feature_aliases

    def test_component_diff_with_empty_lists(self):
        from aegis_ai_web.src.endpoints.kpi import _component_diff

        suggested = []
        submitted = []

        added, removed = _component_diff(suggested, submitted)

        assert added == []
        assert removed == []

    def test_component_diff_with_empty_suggested(self):
        from aegis_ai_web.src.endpoints.kpi import _component_diff

        suggested = []
        submitted = ["component-a", "component-b"]

        added, removed = _component_diff(suggested, submitted)

        assert added == []
        assert removed == ["component-a", "component-b"]

    def test_component_diff_with_empty_submitted(self):
        from aegis_ai_web.src.endpoints.kpi import _component_diff

        suggested = ["component-a", "component-b"]
        submitted = []

        added, removed = _component_diff(suggested, submitted)

        assert added == ["component-a", "component-b"]
        assert removed == []

    def test_component_diff_ignores_empty_and_whitespace_entries(self):
        from aegis_ai_web.src.endpoints.kpi import _component_diff

        suggested = ["", "   ", "component-a"]
        submitted = ["   ", "", "component-b"]

        added, removed = _component_diff(suggested, submitted)

        # Empty / whitespace-only entries should be ignored
        assert added == ["component-a"]
        assert removed == ["component-b"]

    def test_component_diff_with_duplicate_component_names(self):
        from aegis_ai_web.src.endpoints.kpi import _component_diff

        suggested = ["component-a", "component-a", "component-b"]
        submitted = ["component-b", "component-b", "component-c"]

        added, removed = _component_diff(suggested, submitted)

        # Duplicates should not result in duplicate diffs, and order should be stable
        assert added == ["component-a"]
        assert removed == ["component-c"]

```

If `_component_diff` currently returns a different structure (e.g. a dict or a single list), adjust the test unpacking (`added, removed = ...`) and assertions to match the actual return type.
If `_component_diff` does not yet normalize/trim whitespace or de-duplicate component names, implement that behavior in `aegis_ai_web/src/endpoints/kpi.py` so that:
1. Empty (`""`) and whitespace-only strings are filtered out before diffing.
2. Each component name appears at most once in the `added`/`removed` collections, preserving first-seen order.
</issue_to_address>

### Comment 2
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1292-1301" />
<code_context>
+        assert data.entries[0].accepted is False
+        assert data.acceptance_percentage == 0.0
+
+    def test_filter_by_source_component(self, feedback_log_setup):
+        from aegis_ai_web.src.endpoints.kpi import get_cve_kpi
+
+        self._write_manual_rows(feedback_log_setup, self._component_rows())
+
+        result = get_cve_kpi(
+            "source_component",
+            source_component="curl",
+            detail=True,
+        )
+        data = result["source_component"]
+
+        assert len(data.entries) == 1
+        assert data.entries[0].cve_id == "CVE-2025-1003"
+        assert data.entries[0].suggested_components == ["kernel", "curl"]
+
+    def test_filter_multiple_source_components(self, feedback_log_setup):
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for case-insensitive `source_component` filtering

The new `_matches_entry_filters` logic lowercases and trims component names, but `test_filter_by_source_component` only covers an exact-case match (`source_component="curl"`). Add a test where the CSV contains e.g. `"kernel"` and `"curl"`, but the query uses a different casing (e.g. `source_component="Curl"`), and assert the entry is still returned to verify and lock in the case-insensitive behavior.
</issue_to_address>

### Comment 3
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1387-1396" />
<code_context>
+        assert data.entries[0].cve_id == "CVE-2025-2001"
+        assert data.entries[0].suggested_components == ["openssl"]
+
+    def test_filters_recompute_acceptance_percentage(self, feedback_log_setup):
+        from aegis_ai_web.src.endpoints.kpi import get_cve_kpi
+
+        self._write_manual_rows(feedback_log_setup, self._component_rows())
+
+        unfiltered = get_cve_kpi("source_component")
+        filtered = get_cve_kpi(
+            "source_component",
+            source_component="curl",
+        )
+
+        assert unfiltered["source_component"].acceptance_percentage == 33.3
+        assert len(unfiltered["source_component"].entries) == 3
+        assert filtered["source_component"].acceptance_percentage == 0.0
+        assert len(filtered["source_component"].entries) == 1
+
+    def test_includes_scored_programmatic_component_feedback(
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a mixed manual+programmatic filter test to verify recomputed acceptance percentages

The current test only exercises manual feedback. Because `_collect_normalized_entries` now mixes manual and deduplicated programmatic entries, please add a case where both exist for the same feature and filters are applied (e.g., one manual accepted, one programmatic rejected, then filter by `source_component` or `cve_id`). Asserting the acceptance percentage and entry count in that mixed scenario will better validate the aggregation logic on realistic data.

Suggested implementation:

```python
    def test_filters_recompute_acceptance_percentage_mixed_manual_programmatic(
        self, feedback_log_setup, programmatic_feedback_log_setup
    ):
        from aegis_ai_web.src.endpoints.kpi import get_cve_kpi

        # One manual accepted feedback for a CVE/component
        self._write_manual_rows(
            feedback_log_setup,
            [
                {
                    "datetime": "2026-03-21 09:00:00.000",
                    "feature": "source_component",
                    "value": "curl",
                    "cve_id": "CVE-2025-2001",
                    "accepted": True,
                },
            ],
        )

        # One programmatic rejected feedback for the same CVE/component
        self._write_programmatic_rows(
            programmatic_feedback_log_setup,
            [
                {
                    "datetime": "2026-03-21 10:00:00.123",
                    "feature": "source_component",
                    "value": "curl",
                    "cve_id": "CVE-2025-2001",
                    "accepted": False,
                    "score": 0.0,
                },
            ],
        )

        # Unfiltered KPI should include both manual and programmatic feedback
        unfiltered = get_cve_kpi("source_component")
        # Filtered KPI should still include both entries, but constrained to the component
        filtered = get_cve_kpi(
            "source_component",
            source_component="curl",
        )

        assert len(unfiltered["source_component"].entries) == 2
        assert len(filtered["source_component"].entries) == 2
        # One accepted, one rejected → 50% acceptance in the mixed scenario
        assert filtered["source_component"].acceptance_percentage == 50.0

    def test_includes_scored_programmatic_component_feedback(
        self, feedback_log_setup, programmatic_feedback_log_setup
    ):
        from aegis_ai_web.src.endpoints.kpi import get_cve_kpi

        self._write_programmatic_rows(
            programmatic_feedback_log_setup,
            [
                {
                    "datetime": "2026-03-21 10:00:00.123",
                    "feature": "source_component",

```

The new mixed manual+programmatic filter test assumes the following, which you should align with the existing helpers and KPI implementation:
1. Ensure `_write_manual_rows` and `_write_programmatic_rows` accept the keys used here (`value`, `cve_id`, `accepted`, `score`). If your existing tests use different keys (e.g., `source_component` instead of `value`, or `is_accepted`), adjust the new test’s dictionaries accordingly.
2. Confirm how acceptance percentage is computed when manual and programmatic entries are mixed. If programmatic entries are deduplicated or merged with manual ones, you may need to tweak the expected `len(...entries)` and `acceptance_percentage` values to reflect the actual aggregation logic.
3. If filtering by `source_component` uses a different parameter name or filter field (e.g., `component` or `suggested_component`), update the `get_cve_kpi` calls in the new test to match the existing API.
</issue_to_address>

### Comment 4
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1359-1368" />
<code_context>
+    def test_feature_alias_suggest_affected_components(self, feedback_log_setup):
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for `feature='all'` combined with component feature aliases and filters

Since `_get_all_features_kpi` now uses `_collect_normalized_entries` with alias handling and filters, please add a complementary test that exercises the `feature='all'` path. For example, call `get_cve_kpi("all", detail=True, source_component="openssl")` and assert that both `"source_component"` and `"suggest-affected-components"` (or the expected feature set) are present with correctly filtered entries. This will validate alias resolution and filtering when multiple features are requested.

Suggested implementation:

```python
    def test_feature_alias_suggest_affected_components(self, feedback_log_setup):
        from aegis_ai_web.src.endpoints.kpi import get_cve_kpi

        self._write_manual_rows(
            feedback_log_setup,
            [
                {
                    "datetime": "2026-03-19 21:52:24.452",
                    "feature": "suggest-affected-components",
                    "cve_id": "CVE-2025-2001",
                    "email": "analyst@example.com",
                },
            ],
        )

        result = get_cve_kpi("suggest-affected-components", detail=True)
        suggest_entries = result["suggest-affected-components"].entries

        assert len(suggest_entries) > 0
        for entry in suggest_entries:
            payload = entry.model_dump(exclude_none=True)
            assert payload["cve_id"] == "CVE-2025-2001"

    def test_feature_all_with_alias_and_filters(self, feedback_log_setup):
        from aegis_ai_web.src.endpoints.kpi import get_cve_kpi

        # Write mixed feature rows, with component filter and alias
        self._write_manual_rows(
            feedback_log_setup,
            [
                {
                    "datetime": "2026-03-19 21:52:24.452",
                    "feature": "source_component",
                    "cve_id": "CVE-2025-2001",
                    "email": "analyst@example.com",
                    "source_component": "openssl",
                    "accepted": True,
                    "aegis_version": "0.6.1",
                },
                {
                    "datetime": "2026-03-19 21:52:24.452",
                    "feature": "suggest-affected-components",
                    "cve_id": "CVE-2025-2002",
                    "email": "analyst@example.com",
                    "source_component": "openssl",
                    "accepted": True,
                    "aegis_version": "0.6.1",
                },
                {
                    # This entry should be filtered out by source_component="openssl"
                    "datetime": "2026-03-19 21:52:24.452",
                    "feature": "suggest-affected-components",
                    "cve_id": "CVE-2025-2003",
                    "email": "analyst@example.com",
                    "source_component": "nginx",
                    "accepted": True,
                    "aegis_version": "0.6.1",
                },
            ],
        )

        result = get_cve_kpi("all", detail=True, source_component="openssl")

        # Ensure both features are present in the aggregated result
        assert "source_component" in result
        assert "suggest-affected-components" in result

        source_entries = result["source_component"].entries
        suggest_entries = result["suggest-affected-components"].entries

        # All entries must be filtered by the requested source_component
        assert len(source_entries) > 0
        assert len(suggest_entries) > 0

        for entry in source_entries:
            payload = entry.model_dump(exclude_none=True)
            assert payload["source_component"] == "openssl"
            assert payload["cve_id"] == "CVE-2025-2001"

        for entry in suggest_entries:
            payload = entry.model_dump(exclude_none=True)
            assert payload["source_component"] == "openssl"
            # Only the openssl entry should remain, nginx should be filtered out
            assert payload["cve_id"] == "CVE-2025-2002"

```

I only see part of `test_feature_alias_suggest_affected_components`, so you may need to:
1. Remove any duplicated or conflicting code inside that test if it already has assertions or a different setup further down in the file.
2. Ensure that the structure of `feedback_log_setup` and `self._write_manual_rows` matches these fields (`source_component`, `accepted`, `aegis_version`). If your test helpers use different field names, adjust the dictionaries accordingly.
3. If `get_cve_kpi("all", ...)` returns a different shape (e.g., attributes instead of dict keys, or different feature names/aliases), update the assertions to match your actual return type and alias resolution.
4. If the datetime/aegis_version/email fields are mandatory or validated in other tests, keep them consistent here or reuse existing test fixtures/constants instead of hardcoding values.
</issue_to_address>

### Comment 5
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1403-1342" />
<code_context>
+    def test_includes_scored_programmatic_component_feedback(
</code_context>
<issue_to_address>
**suggestion (testing):** Add an explicit test to confirm unscored programmatic entries are excluded

To fully validate `_normalize_programmatic_entry`, please add a test CSV fixture that includes both a valid scored programmatic entry and entries with empty or non-numeric `acceptance_score`. The test should assert that only the scored entry appears in `get_cve_kpi("source_component", detail=True)` results, confirming unscored programmatic feedback is excluded.
</issue_to_address>

### Comment 6
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1484-1493" />
<code_context>
+            "aegis_version",
+        }
+
+    def test_api_detail_and_filters(self, feedback_log_setup):
+        TestComponentKpiFilters._write_manual_rows(
+            feedback_log_setup, TestComponentKpiFilters._component_rows()
+        )
+
+        response = client.get(
+            "/api/v1/analysis/kpi/cve"
+            "?feature=source_component"
+            "&detail=true"
+            "&cve_id=CVE-2025-1002"
+        )
+        assert response.status_code == 200
+        entry = response.json()["source_component"]["entries"][0]
+        assert entry["cve_id"] == "CVE-2025-1002"
+        assert entry["suggested_components"] == ["kernel"]
+        assert entry["submitted_components"] == ["linux-kernel"]
+        assert entry["rejected_suggestions"] == ["kernel"]
+        assert entry["added_components"] == ["linux-kernel"]
+
+    def test_api_multiple_source_components_filter(self, feedback_log_setup):
</code_context>
<issue_to_address>
**suggestion (testing):** Consider an API-level test to assert `response_model_exclude_none` behavior with `detail=true`

This test verifies enriched fields with `detail=true` and filters, but does not assert that `None` fields are excluded from the JSON when `response_model_exclude_none=True`. Please add a focused API test where some optional detail fields are unset (e.g., `email`, `accepted_components`), and assert that these keys are absent from the response to cover the null-omission behavior.

Suggested implementation:

```python
    def test_api_detail_and_filters(self, feedback_log_setup):
        TestComponentKpiFilters._write_manual_rows(
            feedback_log_setup, TestComponentKpiFilters._component_rows()
        )

        response = client.get(
            "/api/v1/analysis/kpi/cve"
            "?feature=source_component"
            "&detail=true"
            "&cve_id=CVE-2025-1002"
        )
        assert response.status_code == 200
        entry = response.json()["source_component"]["entries"][0]
        assert entry["cve_id"] == "CVE-2025-1002"
        assert entry["suggested_components"] == ["kernel"]
        assert entry["submitted_components"] == ["linux-kernel"]
        assert entry["rejected_suggestions"] == ["kernel"]
        assert entry["added_components"] == ["linux-kernel"]

    def test_api_detail_excludes_none_fields(self, feedback_log_setup):
        """
        Assert response_model_exclude_none behavior with detail=true:
        optional fields that are unset/None (e.g., email, accepted_components)
        must be omitted from the JSON payload.
        """
        TestComponentKpiFilters._write_manual_rows(
            feedback_log_setup,
            TestComponentKpiFilters._component_rows_with_missing_optional_fields(),
        )

        response = client.get(
            "/api/v1/analysis/kpi/cve"
            "?feature=source_component"
            "&detail=true"
        )
        assert response.status_code == 200
        data = response.json()
        assert "source_component" in data
        assert data["source_component"]["entries"], "Expected at least one entry"

        entry = data["source_component"]["entries"][0]

        # Optional fields that are unset/None in the test data
        # must not be present when response_model_exclude_none=True.
        assert "email" not in entry
        assert "accepted_components" not in entry

    def test_api_multiple_source_components_filter(self, feedback_log_setup):
        TestComponentKpiFilters._write_manual_rows(
            feedback_log_setup, TestComponentKpiFilters._component_rows()
        )

        response = client.get(
            "/api/v1/analysis/kpi/cve"
            "?feature=source_component"
            "&multiple_source_components=true"
            "&detail=true"
        )

```

To make `test_api_detail_excludes_none_fields` pass and correctly exercise `response_model_exclude_none`, you will need to:
1. Implement `TestComponentKpiFilters._component_rows_with_missing_optional_fields()` in the same test module (or wherever `TestComponentKpiFilters` is defined). It should return rows where the detail-level optional fields such as `email` and `accepted_components` are explicitly unset or set to `None` for at least one entry.
2. Ensure that these rows, when processed by the API, would normally populate `email` and `accepted_components` when present, so that the absence of these keys in the response is specifically due to `response_model_exclude_none=True` and not because the fields don't exist in the model.
</issue_to_address>

### Comment 7
<location path="src/aegis_ai_web/tests/test_kpi.py" line_range="1503-1512" />
<code_context>
+        assert entry["rejected_suggestions"] == ["kernel"]
+        assert entry["added_components"] == ["linux-kernel"]
+
+    def test_api_multiple_source_components_filter(self, feedback_log_setup):
+        TestComponentKpiFilters._write_manual_rows(
+            feedback_log_setup, TestComponentKpiFilters._component_rows()
+        )
+
+        response = client.get(
+            "/api/v1/analysis/kpi/cve"
+            "?feature=source_component"
+            "&multiple_source_components=true"
+            "&detail=true"
+        )
+        assert response.status_code == 200
+        entries = response.json()["source_component"]["entries"]
+        assert len(entries) == 1
+        assert entries[0]["cve_id"] == "CVE-2025-1003"
+
+    def test_api_invalid_cve_id_returns_422(self, feedback_log_setup):
</code_context>
<issue_to_address>
**suggestion (testing):** Add a negative API test for `multiple_source_components` with single-component suggestions

This test currently exercises the positive case where `multiple_source_components=true` yields an entry with two suggested components. Please also add an API test combining `multiple_source_components=true` with a `source_component` or `cve_id` that only occurs in single-component suggestions, and assert that the response has an empty `entries` list and `acceptance_percentage` 0.0. This will mirror `test_no_matches_returns_empty_result` at the HTTP layer and verify consistent behavior for this filter.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +1157 to +1166
class TestComponentKpiHelpers:
"""Unit tests for component KPI helper functions."""

def test_resolve_feature_aliases_for_source_component(self):
from aegis_ai_web.src.endpoints.kpi import _resolve_feature_aliases

aliases = _resolve_feature_aliases("source_component")
assert aliases == {"source_component", "suggest-affected-components"}

def test_resolve_feature_aliases_for_unrelated_feature(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add edge-case coverage for _component_diff (empty, whitespace, duplicates)

Please add tests for _component_diff where:

  • suggested and/or submitted are empty lists
  • entries are empty strings or whitespace only
  • arrays contain duplicate component names (to verify expected handling/order)

These will clarify the intended semantics under noisy input and ensure the helper behaves correctly in common edge cases.

Suggested implementation:

class TestComponentKpiHelpers:
    """Unit tests for component KPI helper functions."""

    def test_resolve_feature_aliases_for_source_component(self):
        from aegis_ai_web.src.endpoints.kpi import _resolve_feature_aliases

        aliases = _resolve_feature_aliases("source_component")
        assert aliases == {"source_component", "suggest-affected-components"}

    def test_resolve_feature_aliases_for_unrelated_feature(self):
        from aegis_ai_web.src.endpoints.kpi import _resolve_feature_aliases

    def test_component_diff_with_empty_lists(self):
        from aegis_ai_web.src.endpoints.kpi import _component_diff

        suggested = []
        submitted = []

        added, removed = _component_diff(suggested, submitted)

        assert added == []
        assert removed == []

    def test_component_diff_with_empty_suggested(self):
        from aegis_ai_web.src.endpoints.kpi import _component_diff

        suggested = []
        submitted = ["component-a", "component-b"]

        added, removed = _component_diff(suggested, submitted)

        assert added == []
        assert removed == ["component-a", "component-b"]

    def test_component_diff_with_empty_submitted(self):
        from aegis_ai_web.src.endpoints.kpi import _component_diff

        suggested = ["component-a", "component-b"]
        submitted = []

        added, removed = _component_diff(suggested, submitted)

        assert added == ["component-a", "component-b"]
        assert removed == []

    def test_component_diff_ignores_empty_and_whitespace_entries(self):
        from aegis_ai_web.src.endpoints.kpi import _component_diff

        suggested = ["", "   ", "component-a"]
        submitted = ["   ", "", "component-b"]

        added, removed = _component_diff(suggested, submitted)

        # Empty / whitespace-only entries should be ignored
        assert added == ["component-a"]
        assert removed == ["component-b"]

    def test_component_diff_with_duplicate_component_names(self):
        from aegis_ai_web.src.endpoints.kpi import _component_diff

        suggested = ["component-a", "component-a", "component-b"]
        submitted = ["component-b", "component-b", "component-c"]

        added, removed = _component_diff(suggested, submitted)

        # Duplicates should not result in duplicate diffs, and order should be stable
        assert added == ["component-a"]
        assert removed == ["component-c"]

If _component_diff currently returns a different structure (e.g. a dict or a single list), adjust the test unpacking (added, removed = ...) and assertions to match the actual return type.
If _component_diff does not yet normalize/trim whitespace or de-duplicate component names, implement that behavior in aegis_ai_web/src/endpoints/kpi.py so that:

  1. Empty ("") and whitespace-only strings are filtered out before diffing.
  2. Each component name appears at most once in the added/removed collections, preserving first-seen order.

Comment on lines +1292 to +1301
def test_filter_by_source_component(self, feedback_log_setup):
from aegis_ai_web.src.endpoints.kpi import get_cve_kpi

self._write_manual_rows(feedback_log_setup, self._component_rows())

result = get_cve_kpi(
"source_component",
source_component="curl",
detail=True,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add a test for case-insensitive source_component filtering

The new _matches_entry_filters logic lowercases and trims component names, but test_filter_by_source_component only covers an exact-case match (source_component="curl"). Add a test where the CSV contains e.g. "kernel" and "curl", but the query uses a different casing (e.g. source_component="Curl"), and assert the entry is still returned to verify and lock in the case-insensitive behavior.

Comment on lines +1387 to +1396
def test_filters_recompute_acceptance_percentage(self, feedback_log_setup):
from aegis_ai_web.src.endpoints.kpi import get_cve_kpi

self._write_manual_rows(feedback_log_setup, self._component_rows())

unfiltered = get_cve_kpi("source_component")
filtered = get_cve_kpi(
"source_component",
source_component="curl",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding a mixed manual+programmatic filter test to verify recomputed acceptance percentages

The current test only exercises manual feedback. Because _collect_normalized_entries now mixes manual and deduplicated programmatic entries, please add a case where both exist for the same feature and filters are applied (e.g., one manual accepted, one programmatic rejected, then filter by source_component or cve_id). Asserting the acceptance percentage and entry count in that mixed scenario will better validate the aggregation logic on realistic data.

Suggested implementation:

    def test_filters_recompute_acceptance_percentage_mixed_manual_programmatic(
        self, feedback_log_setup, programmatic_feedback_log_setup
    ):
        from aegis_ai_web.src.endpoints.kpi import get_cve_kpi

        # One manual accepted feedback for a CVE/component
        self._write_manual_rows(
            feedback_log_setup,
            [
                {
                    "datetime": "2026-03-21 09:00:00.000",
                    "feature": "source_component",
                    "value": "curl",
                    "cve_id": "CVE-2025-2001",
                    "accepted": True,
                },
            ],
        )

        # One programmatic rejected feedback for the same CVE/component
        self._write_programmatic_rows(
            programmatic_feedback_log_setup,
            [
                {
                    "datetime": "2026-03-21 10:00:00.123",
                    "feature": "source_component",
                    "value": "curl",
                    "cve_id": "CVE-2025-2001",
                    "accepted": False,
                    "score": 0.0,
                },
            ],
        )

        # Unfiltered KPI should include both manual and programmatic feedback
        unfiltered = get_cve_kpi("source_component")
        # Filtered KPI should still include both entries, but constrained to the component
        filtered = get_cve_kpi(
            "source_component",
            source_component="curl",
        )

        assert len(unfiltered["source_component"].entries) == 2
        assert len(filtered["source_component"].entries) == 2
        # One accepted, one rejected → 50% acceptance in the mixed scenario
        assert filtered["source_component"].acceptance_percentage == 50.0

    def test_includes_scored_programmatic_component_feedback(
        self, feedback_log_setup, programmatic_feedback_log_setup
    ):
        from aegis_ai_web.src.endpoints.kpi import get_cve_kpi

        self._write_programmatic_rows(
            programmatic_feedback_log_setup,
            [
                {
                    "datetime": "2026-03-21 10:00:00.123",
                    "feature": "source_component",

The new mixed manual+programmatic filter test assumes the following, which you should align with the existing helpers and KPI implementation:

  1. Ensure _write_manual_rows and _write_programmatic_rows accept the keys used here (value, cve_id, accepted, score). If your existing tests use different keys (e.g., source_component instead of value, or is_accepted), adjust the new test’s dictionaries accordingly.
  2. Confirm how acceptance percentage is computed when manual and programmatic entries are mixed. If programmatic entries are deduplicated or merged with manual ones, you may need to tweak the expected len(...entries) and acceptance_percentage values to reflect the actual aggregation logic.
  3. If filtering by source_component uses a different parameter name or filter field (e.g., component or suggested_component), update the get_cve_kpi calls in the new test to match the existing API.

Comment on lines +1359 to +1368
def test_feature_alias_suggest_affected_components(self, feedback_log_setup):
from aegis_ai_web.src.endpoints.kpi import get_cve_kpi

self._write_manual_rows(
feedback_log_setup,
[
{
"datetime": "2026-03-19 21:52:24.452",
"feature": "suggest-affected-components",
"cve_id": "CVE-2025-2001",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add coverage for feature='all' combined with component feature aliases and filters

Since _get_all_features_kpi now uses _collect_normalized_entries with alias handling and filters, please add a complementary test that exercises the feature='all' path. For example, call get_cve_kpi("all", detail=True, source_component="openssl") and assert that both "source_component" and "suggest-affected-components" (or the expected feature set) are present with correctly filtered entries. This will validate alias resolution and filtering when multiple features are requested.

Suggested implementation:

    def test_feature_alias_suggest_affected_components(self, feedback_log_setup):
        from aegis_ai_web.src.endpoints.kpi import get_cve_kpi

        self._write_manual_rows(
            feedback_log_setup,
            [
                {
                    "datetime": "2026-03-19 21:52:24.452",
                    "feature": "suggest-affected-components",
                    "cve_id": "CVE-2025-2001",
                    "email": "analyst@example.com",
                },
            ],
        )

        result = get_cve_kpi("suggest-affected-components", detail=True)
        suggest_entries = result["suggest-affected-components"].entries

        assert len(suggest_entries) > 0
        for entry in suggest_entries:
            payload = entry.model_dump(exclude_none=True)
            assert payload["cve_id"] == "CVE-2025-2001"

    def test_feature_all_with_alias_and_filters(self, feedback_log_setup):
        from aegis_ai_web.src.endpoints.kpi import get_cve_kpi

        # Write mixed feature rows, with component filter and alias
        self._write_manual_rows(
            feedback_log_setup,
            [
                {
                    "datetime": "2026-03-19 21:52:24.452",
                    "feature": "source_component",
                    "cve_id": "CVE-2025-2001",
                    "email": "analyst@example.com",
                    "source_component": "openssl",
                    "accepted": True,
                    "aegis_version": "0.6.1",
                },
                {
                    "datetime": "2026-03-19 21:52:24.452",
                    "feature": "suggest-affected-components",
                    "cve_id": "CVE-2025-2002",
                    "email": "analyst@example.com",
                    "source_component": "openssl",
                    "accepted": True,
                    "aegis_version": "0.6.1",
                },
                {
                    # This entry should be filtered out by source_component="openssl"
                    "datetime": "2026-03-19 21:52:24.452",
                    "feature": "suggest-affected-components",
                    "cve_id": "CVE-2025-2003",
                    "email": "analyst@example.com",
                    "source_component": "nginx",
                    "accepted": True,
                    "aegis_version": "0.6.1",
                },
            ],
        )

        result = get_cve_kpi("all", detail=True, source_component="openssl")

        # Ensure both features are present in the aggregated result
        assert "source_component" in result
        assert "suggest-affected-components" in result

        source_entries = result["source_component"].entries
        suggest_entries = result["suggest-affected-components"].entries

        # All entries must be filtered by the requested source_component
        assert len(source_entries) > 0
        assert len(suggest_entries) > 0

        for entry in source_entries:
            payload = entry.model_dump(exclude_none=True)
            assert payload["source_component"] == "openssl"
            assert payload["cve_id"] == "CVE-2025-2001"

        for entry in suggest_entries:
            payload = entry.model_dump(exclude_none=True)
            assert payload["source_component"] == "openssl"
            # Only the openssl entry should remain, nginx should be filtered out
            assert payload["cve_id"] == "CVE-2025-2002"

I only see part of test_feature_alias_suggest_affected_components, so you may need to:

  1. Remove any duplicated or conflicting code inside that test if it already has assertions or a different setup further down in the file.
  2. Ensure that the structure of feedback_log_setup and self._write_manual_rows matches these fields (source_component, accepted, aegis_version). If your test helpers use different field names, adjust the dictionaries accordingly.
  3. If get_cve_kpi("all", ...) returns a different shape (e.g., attributes instead of dict keys, or different feature names/aliases), update the assertions to match your actual return type and alias resolution.
  4. If the datetime/aegis_version/email fields are mandatory or validated in other tests, keep them consistent here or reuse existing test fixtures/constants instead of hardcoding values.

assert entry.suggested_components == ["kernel"]
assert entry.submitted_components == ["linux-kernel"]
assert entry.rejected_suggestions == ["kernel"]
assert entry.added_components == ["linux-kernel"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add an explicit test to confirm unscored programmatic entries are excluded

To fully validate _normalize_programmatic_entry, please add a test CSV fixture that includes both a valid scored programmatic entry and entries with empty or non-numeric acceptance_score. The test should assert that only the scored entry appears in get_cve_kpi("source_component", detail=True) results, confirming unscored programmatic feedback is excluded.

Comment on lines +1484 to +1493
def test_api_detail_and_filters(self, feedback_log_setup):
TestComponentKpiFilters._write_manual_rows(
feedback_log_setup, TestComponentKpiFilters._component_rows()
)

response = client.get(
"/api/v1/analysis/kpi/cve"
"?feature=source_component"
"&detail=true"
"&cve_id=CVE-2025-1002"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Consider an API-level test to assert response_model_exclude_none behavior with detail=true

This test verifies enriched fields with detail=true and filters, but does not assert that None fields are excluded from the JSON when response_model_exclude_none=True. Please add a focused API test where some optional detail fields are unset (e.g., email, accepted_components), and assert that these keys are absent from the response to cover the null-omission behavior.

Suggested implementation:

    def test_api_detail_and_filters(self, feedback_log_setup):
        TestComponentKpiFilters._write_manual_rows(
            feedback_log_setup, TestComponentKpiFilters._component_rows()
        )

        response = client.get(
            "/api/v1/analysis/kpi/cve"
            "?feature=source_component"
            "&detail=true"
            "&cve_id=CVE-2025-1002"
        )
        assert response.status_code == 200
        entry = response.json()["source_component"]["entries"][0]
        assert entry["cve_id"] == "CVE-2025-1002"
        assert entry["suggested_components"] == ["kernel"]
        assert entry["submitted_components"] == ["linux-kernel"]
        assert entry["rejected_suggestions"] == ["kernel"]
        assert entry["added_components"] == ["linux-kernel"]

    def test_api_detail_excludes_none_fields(self, feedback_log_setup):
        """
        Assert response_model_exclude_none behavior with detail=true:
        optional fields that are unset/None (e.g., email, accepted_components)
        must be omitted from the JSON payload.
        """
        TestComponentKpiFilters._write_manual_rows(
            feedback_log_setup,
            TestComponentKpiFilters._component_rows_with_missing_optional_fields(),
        )

        response = client.get(
            "/api/v1/analysis/kpi/cve"
            "?feature=source_component"
            "&detail=true"
        )
        assert response.status_code == 200
        data = response.json()
        assert "source_component" in data
        assert data["source_component"]["entries"], "Expected at least one entry"

        entry = data["source_component"]["entries"][0]

        # Optional fields that are unset/None in the test data
        # must not be present when response_model_exclude_none=True.
        assert "email" not in entry
        assert "accepted_components" not in entry

    def test_api_multiple_source_components_filter(self, feedback_log_setup):
        TestComponentKpiFilters._write_manual_rows(
            feedback_log_setup, TestComponentKpiFilters._component_rows()
        )

        response = client.get(
            "/api/v1/analysis/kpi/cve"
            "?feature=source_component"
            "&multiple_source_components=true"
            "&detail=true"
        )

To make test_api_detail_excludes_none_fields pass and correctly exercise response_model_exclude_none, you will need to:

  1. Implement TestComponentKpiFilters._component_rows_with_missing_optional_fields() in the same test module (or wherever TestComponentKpiFilters is defined). It should return rows where the detail-level optional fields such as email and accepted_components are explicitly unset or set to None for at least one entry.
  2. Ensure that these rows, when processed by the API, would normally populate email and accepted_components when present, so that the absence of these keys in the response is specifically due to response_model_exclude_none=True and not because the fields don't exist in the model.

Comment on lines +1503 to +1512
def test_api_multiple_source_components_filter(self, feedback_log_setup):
TestComponentKpiFilters._write_manual_rows(
feedback_log_setup, TestComponentKpiFilters._component_rows()
)

response = client.get(
"/api/v1/analysis/kpi/cve"
"?feature=source_component"
"&multiple_source_components=true"
"&detail=true"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add a negative API test for multiple_source_components with single-component suggestions

This test currently exercises the positive case where multiple_source_components=true yields an entry with two suggested components. Please also add an API test combining multiple_source_components=true with a source_component or cve_id that only occurs in single-component suggestions, and assert that the response has an empty entries list and acceptance_percentage 0.0. This will mirror test_no_matches_returns_empty_result at the HTTP layer and verify consistent behavior for this filter.

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aegis_ai_web/src/data_models.py`:
- Around line 318-320: The KPI response model currently exposes a raw email
field, which should not be part of the public contract. Remove the email
attribute from the response schema in Data models (the model defining the KPI
feedback/detail payload) and update the KPI endpoint logic in kpi.py to stop
populating it; if correlation is still needed, replace it with a pseudonymous
identifier or keep the value server-side only.

In `@src/aegis_ai_web/src/endpoints/kpi.py`:
- Around line 33-37: The alias resolution in _resolve_feature_aliases is
expanding both feature values into the full COMPONENT_FEATURE_KEYS set, which
unintentionally changes the legacy suggest-affected-components KPI scope. Update
the logic so only source_component maps to the broader component-feature union,
while suggest-affected-components continues to resolve to its original single
key. Keep the change localized to _resolve_feature_aliases and verify any
callers in the KPI endpoint still receive the expected one-way alias behavior.
🪄 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: Enterprise

Run ID: e433b03f-aa11-4b67-9987-eca57548235c

📥 Commits

Reviewing files that changed from the base of the PR and between 8207772 and 0948990.

📒 Files selected for processing (5)
  • docs/CHANGELOG.md
  • src/aegis_ai_web/src/data_models.py
  • src/aegis_ai_web/src/endpoints/kpi.py
  • src/aegis_ai_web/src/main.py
  • src/aegis_ai_web/tests/test_kpi.py

Comment on lines +318 to +320
email: Optional[str] = Field(
default=None,
description="User email from feedback (included when detail=true)",

@coderabbitai coderabbitai Bot Jun 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove raw email from the KPI response model.

detail=true now makes the feedback submitter's email part of the API contract, and src/aegis_ai_web/src/endpoints/kpi.py actively populates it. That's unnecessary PII for a KPI endpoint and increases disclosure/retention risk. Prefer a pseudonymous identifier, or keep email server-side unless this route is explicitly admin-only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aegis_ai_web/src/data_models.py` around lines 318 - 320, The KPI response
model currently exposes a raw email field, which should not be part of the
public contract. Remove the email attribute from the response schema in Data
models (the model defining the KPI feedback/detail payload) and update the KPI
endpoint logic in kpi.py to stop populating it; if correlation is still needed,
replace it with a pseudonymous identifier or keep the value server-side only.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@jvang-rh I tend to agree. If I remember correctly, we did not expose all the captured data on purpose because it may contain sensitive information. Moreover, with the email field specifically, we may need to retake PIA, etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@kdudka Thanks for confirming — that context is helpful. Given the PIA implications, it'd be safer to drop email from the KPIEntry response model entirely (or replace it with a non-identifying hash/token if correlation across entries is genuinely needed) rather than exposing it behind detail=true. Happy to help review once the field is removed or reworked.


📝 Learnings were identified and not saved (knowledge base disabled). Enable

Comment on lines +33 to +37
def _resolve_feature_aliases(query_feature: str) -> set[str]:
"""Map component feature aliases to the same CSV feature keys."""
if query_feature in COMPONENT_FEATURE_KEYS:
return set(COMPONENT_FEATURE_KEYS)
return {query_feature}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep source_component aliasing one-way.

This expands both feature=source_component and feature=suggest-affected-components to the union of both log keys. That changes the legacy suggest-affected-components filter from “that feature only” to “both component features”, while the PR objective/changelog only describe source_component as the alias. Restrict the expansion to source_component so existing callers keep the old KPI scope.

Proposed fix
 def _resolve_feature_aliases(query_feature: str) -> set[str]:
     """Map component feature aliases to the same CSV feature keys."""
-    if query_feature in COMPONENT_FEATURE_KEYS:
+    if query_feature == "source_component":
         return set(COMPONENT_FEATURE_KEYS)
     return {query_feature}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _resolve_feature_aliases(query_feature: str) -> set[str]:
"""Map component feature aliases to the same CSV feature keys."""
if query_feature in COMPONENT_FEATURE_KEYS:
return set(COMPONENT_FEATURE_KEYS)
return {query_feature}
def _resolve_feature_aliases(query_feature: str) -> set[str]:
"""Map component feature aliases to the same CSV feature keys."""
if query_feature == "source_component":
return set(COMPONENT_FEATURE_KEYS)
return {query_feature}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aegis_ai_web/src/endpoints/kpi.py` around lines 33 - 37, The alias
resolution in _resolve_feature_aliases is expanding both feature values into the
full COMPONENT_FEATURE_KEYS set, which unintentionally changes the legacy
suggest-affected-components KPI scope. Update the logic so only source_component
maps to the broader component-feature union, while suggest-affected-components
continues to resolve to its original single key. Keep the change localized to
_resolve_feature_aliases and verify any callers in the KPI endpoint still
receive the expected one-way alias behavior.

@superbuggy
superbuggy requested review from kdudka and superbuggy June 30, 2026 12:12
@superbuggy

Copy link
Copy Markdown
Collaborator
  1. make format will resolve formatting errors.
  2. uv run pytest tests will show what tests are failing locally.
  3. One of us can show you how to request/obtain an api key we use; we don't use any openai models in the evaluation suite pipeline.

Will provide more in-depth feedback shortly

@kdudka kdudka left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@jvang-rh Thank you for working on this!

Ideally, the pull request should be opened from an upstream branch so that the CI pipelines have access to our secrets. Unfortunately, I am not able to grant you access in the project settings.

I guess you need to become a member of the RedHatProductSecurity group first?

) -> tuple[list[str], list[str], list[str]]:
"""Return accepted, rejected, and added component lists (case-insensitive)."""
suggested_map = {c.lower().strip(): c for c in suggested if c.strip()}
submitted_map = {c.lower().strip(): c for c in submitted if c.strip()}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am not sure about the .lower() normalization. So far Aegis treated components as case sensitive (e.g. Glib and glib as two distinct components).

Comment on lines +318 to +320
email: Optional[str] = Field(
default=None,
description="User email from feedback (included when detail=true)",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@jvang-rh I tend to agree. If I remember correctly, we did not expose all the captured data on purpose because it may contain sensitive information. Moreover, with the email field specifically, we may need to retake PIA, etc.

@kdudka
kdudka marked this pull request as draft July 9, 2026 14:34
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.

3 participants