feat(sast): tolerate hygiene-only Capella reductions instead of going partial

A reduction only makes a run partial when it loses real coverage or a whole
finding. Malformed model output, salvaged turn-limit work, and rejected duplicate
verdicts are recorded as evidence but no longer flip the run to partial.

- add reductionIsTolerable: partial only when genuine-loss counts are nonzero
- drive runCapella's partial reasons and display coverage off non-tolerable ones
- keep every reduction in agenticSast.reductions so nothing is lost as evidence
This commit is contained in:
ajmallesh
2026-08-27 18:40:36 -07:00
parent 098bf4be05
commit 8bab4ccb1b
2 changed files with 56 additions and 12 deletions
+20 -12
View File
@@ -49,6 +49,7 @@ import {
type PartialReason,
partialReasonFromReduction,
projectPartialReasons,
reductionIsTolerable,
renderSafeMessage,
reportIsAuthored,
} from '../types/run-state.js';
@@ -939,28 +940,35 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
const metricKey = result.status === 'succeeded' ? 'agentic-sast:export' : `agentic-sast:${result.failedStage}`;
state.operationalMetrics[metricKey] = capellaMetrics(result, input.agenticSast.modelSpec);
if (result.status === 'succeeded') {
// Only reductions that lost real coverage or a whole finding make the run partial;
// hygiene reductions (malformed model output, salvage, rejected duplicates) are recorded
// as evidence but tolerated. Display coverage follows the same split so `shannon status`
// agrees with the terminal state, while `reductions` still carries the full list.
const nonTolerableReductions = (result.reductions ?? []).filter(
(reduction) => !reductionIsTolerable(reduction),
);
state.agenticSast = {
status: 'succeeded',
findingCount: result.findingCount,
sarifSha256: result.sarif.sha256,
coverage: result.coverage,
coverage: nonTolerableReductions.length > 0 ? 'reduced' : 'complete',
warnings: [...result.warnings],
durationMs: result.durationMs,
...(result.reductions !== undefined && { reductions: result.reductions }),
...(result.recoveredFailure !== undefined && { recoveredFailure: result.recoveredFailure }),
};
completeOperation(CAPELLA_OPERATION_KEY, CAPELLA_OPERATION_LABEL, startedAt);
if (result.coverage === 'reduced') {
// One durable reason per reduction (research before export); each renders its own
// bounded safe message. A reduced run with no structured reduction keeps the bare code.
const reasons: PartialReason[] =
result.reductions === undefined || result.reductions.length === 0
? [{ code: 'agentic_sast_reduced' }]
: result.reductions.map(partialReasonFromReduction);
for (const reason of reasons) {
addPartialReason(reason);
addNonFatal({ phase: 'agentic-sast', error: projectPartialReasons([reason])[0]?.message ?? '' });
}
// One durable reason per non-tolerable reduction (research before export); each renders
// its own bounded safe message. A child that reports reduced coverage without any
// structured reduction keeps the bare code, so an unclassified coverage loss is never
// silently accepted.
const coverageReducedWithoutDetail = result.coverage === 'reduced' && (result.reductions ?? []).length === 0;
const reasons = coverageReducedWithoutDetail
? [{ code: 'agentic_sast_reduced' } satisfies PartialReason]
: nonTolerableReductions.map(partialReasonFromReduction);
for (const reason of reasons) {
addPartialReason(reason);
addNonFatal({ phase: 'agentic-sast', error: projectPartialReasons([reason])[0]?.message ?? '' });
}
return result.sarif;
}
+36
View File
@@ -655,6 +655,42 @@ function renderIncompleteResearchReduction(
return `Agentic SAST reviewed ${String(consideredCount)} planned ${fileLabel} during research. ${triageClause}${salvagedClause} The scan continued with reduced static-analysis coverage.`;
}
/**
* Whether a Capella reduction is tolerable — recorded as evidence but not cause for a partial
* run. A reduction is tolerable when all of its genuine coverage- or finding-loss counts are
* zero; salvage counts (work recovered after a turn/session limit) and rejection counts
* (duplicate or unexpected verdicts thrown out) are hygiene, never loss. Architecture and plan
* reductions only ever drop malformed model output, so they are always tolerable. A stage that
* failed outright (`failed_stage_fallback`) or an exported finding dropped whole
* (`malformed_findings`) is never tolerable. The switch is exhaustive so a new reduction reason
* fails the type-check until its loss counts are classified here.
*/
export function reductionIsTolerable(reduction: AgenticSastReduction): boolean {
switch (reduction.reason) {
case 'invalid_architecture_items':
case 'invalid_investigations':
return true;
case 'incomplete_research':
return reduction.triageOmittedCount === 0;
case 'incomplete_dedupe':
return reduction.unreadableCount === 0;
case 'incomplete_review':
return reduction.missingCount + reduction.unreadableCount + reduction.quarantinedCount === 0;
case 'incomplete_critic':
case 'incomplete_confirm':
case 'incomplete_calibrate':
return reduction.missingCount + reduction.unreadableCount === 0;
case 'failed_stage_fallback':
case 'malformed_findings':
return false;
default: {
const _exhaustive: never = reduction;
void _exhaustive;
return false;
}
}
}
/** Build the durable partial reason for one Capella reduction. Export keeps bounded omission detail. */
export function partialReasonFromReduction(reduction: AgenticSastReduction): PartialReason {
const { reason, ...details } = reduction;