Skip to content

fix(meta): bind assessments to exact findings - #702

Open
Harbor404 wants to merge 2 commits into
NVIDIA:mainfrom
Harbor404:fix/meta-review-finding-binding
Open

Harbor404 wants to merge 2 commits into
NVIDIA:mainfrom
Harbor404:fix/meta-review-finding-binding

Conversation

@Harbor404

Copy link
Copy Markdown

Summary

Fixes #667.

Three related meta-review defects: an assessment could be attached to the wrong
finding, the report could not distinguish "model disagreed" from "review never
happened", and findings added during finalization could make the review phase
look failed when nothing was ever required of it.

Signed-off-by: Harbor404 2657212322@qq.com

Changes

Bind an assessment to the exact finding

  • MetaAnalyzerFinding gains an optional finding_id; the prompt and the
    rendered findings include it and ask for it back.
  • apply_filter() prefers finding_id. Responses without one keep the legacy
    location fallback. When several findings share the same exact location and the
    response carries no id, the result is marked missing instead of guessing a
    binding.

Distinguish review outcomes

Each finding's evidence now records
llm_review_outcome=confirmed|disagreed|low-confidence|missing|failed while
keeping the existing llm-unconfirmed behavior for compatibility, so a report
can tell a model disagreement from an omitted decision from a failed call.

Stop finalization from faking a review failure

  • MetaAnalyzerResponse and SkillspectorState gain a meta_review_required
    snapshot, recorded when the analyzer actually ran.
  • The finalizer preserves that snapshot, so AE1/AE7 coverage findings added
    afterwards no longer demand a successful meta_analyzer call.
  • semantic_runtime prefers the snapshot and, for legacy state, ignores
    post-hoc findings tagged coverage. Genuine review gaps still surface through
    finding evidence and the ledger.

Testing

uv run pytest tests/nodes/test_meta_analyzer.py tests/nodes/test_finalize_inspection_ledger.py \
  tests/nodes/test_llm_analyzer_base.py tests/test_semantic_runtime.py -q
# 477 passed

uv run pytest tests/test_reference_destination_kinds.py -q
# 43 passed

uv run pytest tests -q -m 'not integration and not provider'
# 8485 passed, 14 skipped, 134 deselected, 4 xfailed

uv run ruff format --check ... && uv run ruff check ...
# 7 files already formatted; All checks passed!

mypy passes on meta_analyzer.py, semantic_runtime.py, and state.py.
Checking finalize_inspection_ledger.py alone hits a pre-existing error at
line 124 that is outside this diff.

Notes for Reviewer

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

[SkillSpector Review]

Hi @Harbor404, thank you for tackling all three meta-review defects from #667 in one coherent change, especially the false "LLM degraded" caused by coverage findings!

Value and readiness: The meta_review_required snapshot fixes the false degraded status: AE1/AE7 coverage findings added during finalization no longer require a meta call. The updated test_reference_destination_kinds expectation matches #667, and analysis_completeness still reports the real coverage gap. The duplicate-location guard is a sound conservative choice. No LLM response can remove a finding or lower its severity or confidence: confirmation uses max(...), and llm-unconfirmed has no scoring consumer in src/. However, the new finding_id binding accepts whatever id the model returns without checking it. That can move an assessment onto a different rule's finding, or drop an answer the model did give. In addition, the new evidence fields copy free-form model text onto every reviewed finding, which changes terminal and Markdown output for every LLM scan. Not ready to merge yet.

Material findings

  1. [Blocker] src/skillspector/nodes/meta_analyzer.py:439-442, 472, 493: id binding is trusted without checks, and it is the only binding used when an id is present.

    • An item that carries a finding_id is bound to that finding without checking that the item's pattern_id/start_line match it, or that the id was part of the item's batch. Suppose a batch holds an SC4 finding at line 4 and an AST1 finding at line 9, and the model swaps their ids. The AST1 finding then gets the SC4 assessment's text as its message/explanation/remediation. If the model leaves explanation empty, line 493 builds the default text from assessment.get("pattern_id"), so another rule's default explanation lands on this finding. Main could not mix up rules this way, because its keys included rule_id.
    • An item whose id matches no finding (for example, one hex digit wrong in the 40-character id) hits continue at line 442 and is never indexed by location. The finding it reviewed is then reported as missing, even though the model answered with the correct pattern and line.

    Please index every item both by id and by location. Accept the id only when it belongs to that batch's batch.findings and its rule_id (and start_line, when given) match. Otherwise, fall back to the location path, keeping the duplicate guard. Build default text from f.rule_id. Please add tests for swapped ids, an unknown id with a correct location, and an id from another batch. (Finding ids are per-run uuid4 values, and each batch prompt lists only its own ids. So I see no practical way for a prompt-injected batch to target another batch's finding; the batch check is defense in depth.)

  2. [Blocker] meta_analyzer.py:300-305: _review_evidence copies the model's confidence, explanation and remediation into evidence for every reviewed finding. The reports print evidence in full: the terminal output at report.py:1072-1074 (even though message and remediation there are cut to 60 and 150 characters), and Markdown at report.py:1518-1521 (each value inside an inline code span). Every finding in an LLM scan therefore gains an Evidence line that repeats the model's explanation and remediation. Confirmed findings already carry that text as message/explanation/remediation. For disagreed or low-confidence findings, the model's rebuttal, which prompt injection in the scanned skill can steer ("documented installer, safe to run"), now appears under "Evidence" next to a deterministic finding. #667 only asks to distinguish the outcomes, and llm_review_outcome already does that. Please record only llm_review_outcome (and llm_review_confidence, if wanted) and drop the copied explanation and remediation. If the PIC wants the model's reasoning for non-confirmed findings, put it in a clearly labelled field with a length limit.

  3. [Non-blocking] meta_analyzer.py:331-334, 872-875, 910-914: the failure paths now add the llm-unconfirmed tag through _mark_review_failed. On main, findings whose review failed stayed untagged. The PR description says the existing tag behaviour is kept, but this changes it. Two effects:

    • Consumers that act on the tag will treat an LLM outage as model disagreement, which is the conflation #667 asks to remove.
    • suppression.finding_fingerprint hashes tags, so a baseline written with --no-llm no longer matches findings from a run whose LLM call failed. On main, those two outputs produce the same fingerprint.

    Please leave failure paths untagged and rely on llm_review_outcome=failed, or document the new meaning of the tag.

  4. [Non-blocking] meta_analyzer.py:441 and the location dictionaries: if a finding receives several assessments, the last batch wins. This happens with chunked files: findings_in_range places a finding in every chunk that contains its start line, and chunks overlap by 50 lines. Main kept any confirmation. Now a later chunk without the relevant context can flip a confirmed finding to disagreed. Please merge deterministically (confirmed > low-confidence > disagreed) and add a test.

  5. [Non-blocking] cli.py:2252: the transitive merge builds merged_result = {**initial_result, ...}, so meta_review_required reflects only the root scan, while effective_finding_ids is merged from all children. Consider OR-ing the child flags so the field means the same thing in transitive scans.

  6. [Non-blocking] Tests and docs:

    • test_finding_clones_preserve_security_metadata_and_confidence changed from evidence == original.evidence to two spot checks. Please assert that the original evidence is preserved as a subset (returned.evidence.items() >= original.evidence.items()).
    • Please add a semantic_runtime test for a legacy result (no flag) whose only findings are tagged coverage.
    • The apply_filter and meta_analyzer docstrings still describe matching by (file, rule_id) only.

PIC tradeoffs: First, should the model's reasoning for disagreed or low-confidence findings appear in reports at all (item 2)? Showing it helps triage, but it puts text a scanned skill can influence next to deterministic findings. Second, should llm-unconfirmed also mean "review failed" (item 3)?

Verification and gaps: I traced apply_filter, every return path of the meta_analyzer node, finalize_inspection_ledger, semantic_runtime._has_effective_findings, the transitive merge in cli.py, evidence rendering in report.py, and suppression.finding_fingerprint. I confirmed that finding ids come from finding-{uuid4().hex} (models.py:118-120) and that _file is set by parse_response, not by the model. Tests were not run locally per policy. CI: all 6 checks green. Overlap: no file overlap with Harbor404's #700, #701 or #703-#706. #703 also changes the finding output shape (models.py/report.py, different fields), so the two output changes should be coordinated.


Decision: Changes Requested (reviewed head b8c17dc42a83d0cee94fc47863e0184d43c6cff5)

Comment thread src/skillspector/nodes/meta_analyzer.py Outdated
if not pattern_id or not item.get("is_vulnerability", False):
finding_id = item.get("finding_id")
if finding_id:
assessments_by_id[str(finding_id)] = item

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.

[Blocker] The id is accepted without checking that the item's pattern_id/start_line match the finding, or that the id belongs to this batch. If the model swaps ids between an SC4 finding and an AST1 finding, the AST1 finding is relabelled with the SC4 assessment, and line 493 builds the default text from the item's pattern_id. An unknown or mistyped id hits continue here and is never indexed by location, so the reviewed finding is reported as missing. Please validate the id against batch.findings and the finding's rule_id/start_line, fall back to location matching on mismatch, and build default text from f.rule_id.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 01e6cc9. Every item is now indexed by both id and location. An id is accepted only when it belongs to batch.findings and its pattern/file/start (and end when both are present) match that finding; otherwise the item falls back to the unambiguous location path. Default explanation/remediation now use f.rule_id. Added swapped-id, unknown-id-with-correct-location, and cross-batch-id regressions.

Comment thread src/skillspector/nodes/meta_analyzer.py Outdated
"""Preserve original evidence while recording the per-finding review outcome."""
evidence = {**finding.evidence, "llm_review_outcome": outcome}
if item is not None:
for field in ("confidence", "explanation", "remediation"):

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.

[Blocker] This copies the model's explanation/remediation into evidence on every reviewed finding. Terminal output (report.py:1072-1074) and Markdown (report.py:1518-1521) print evidence in full, so every finding in an LLM scan gains an Evidence line that repeats the model's text. Disagreed findings would show a rebuttal that prompt injection in the scanned skill can steer. Please keep only llm_review_outcome (and llm_review_confidence, if wanted).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 01e6cc9. _review_evidence now retains only llm_review_outcome and the numeric llm_review_confidence; model-provided explanation and remediation are not copied into evidence. A regression asserts neither llm_review_explanation nor llm_review_remediation appears.

@Harbor404

Copy link
Copy Markdown
Author

Additional non-blocking items are addressed in 01e6cc9: failed reviews now remain untagged and use llm_review_outcome=failed; overlapping assessments merge as confirmed > low-confidence > disagreed; transitive scans OR child meta_review_required; the clone test asserts the original evidence subset; a legacy coverage-only semantic runtime case was added; and both matching docstrings now describe validated id plus location fallback.

Signed-off-by: Harbor404 <2657212322@qq.com>
Bind ids only when they match the submitted batch location, merge overlapping verdicts deterministically, retain only outcome/confidence evidence, keep failed reviews untagged, and OR transitive meta-review flags.

Signed-off-by: Harbor404 <2657212322@qq.com>
@Harbor404
Harbor404 force-pushed the fix/meta-review-finding-binding branch from 01e6cc9 to c4d34b8 Compare October 3, 2026 01:59
@Harbor404

Copy link
Copy Markdown
Author

Updated the branch on top of current main and added Signed-off-by to all commits for DCO.

The lint failure was a ruff format --check issue in tests/nodes/test_meta_analyzer.py; that file is now formatted. Focused coverage: the full test_meta_analyzer.py module (36 passed).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Meta-review can attach assessments to the wrong finding and misreport review status

2 participants