mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-09-14 14:09:04 +02:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user