mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-18 20:15:14 +02:00
[pitboss] phase 09: M7 — Default-on flip + real-corpus calibration
This commit is contained in:
parent
118cafa535
commit
996bff5983
19 changed files with 1094 additions and 51 deletions
|
|
@ -10,11 +10,14 @@ export interface StartScanBody {
|
|||
mode?: ScanMode;
|
||||
engine_profile?: EngineProfile;
|
||||
/**
|
||||
* Run dynamic verification on findings after the static pass. Default false.
|
||||
* Backend currently accepts the field as a no-op; verification engine lands
|
||||
* in milestone M1 (see .pitboss/dynamic/context.md).
|
||||
* Override dynamic verification for this scan.
|
||||
* true — force on.
|
||||
* false — force off (skip verification; M7 default is on).
|
||||
* absent — use server config default (true since M7).
|
||||
*/
|
||||
verify?: boolean;
|
||||
/** Also verify Confidence < Medium findings. Default false. */
|
||||
verify_all_confidence?: boolean;
|
||||
}
|
||||
|
||||
export function useStartScan() {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
|||
const [scanRoot, setScanRoot] = useState('');
|
||||
const [mode, setMode] = useState<ScanMode>('full');
|
||||
const [engineProfile, setEngineProfile] = useState<EngineProfile>('balanced');
|
||||
const [verify, setVerify] = useState(false);
|
||||
const [noVerify, setNoVerify] = useState(false);
|
||||
|
||||
const handleStart = async () => {
|
||||
const root = scanRoot.trim();
|
||||
|
|
@ -46,7 +46,7 @@ export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
|||
if (root && root !== defaultRoot) body.scan_root = root;
|
||||
if (mode !== 'full') body.mode = mode;
|
||||
body.engine_profile = engineProfile;
|
||||
if (verify) body.verify = true;
|
||||
if (noVerify) body.verify = false;
|
||||
const payload = Object.keys(body).length ? body : undefined;
|
||||
try {
|
||||
await startScan.mutateAsync(payload);
|
||||
|
|
@ -112,18 +112,17 @@ export function NewScanModal({ open, onClose }: NewScanModalProps) {
|
|||
<div className="toggle-inline">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="new-scan-verify"
|
||||
checked={verify}
|
||||
onChange={(e) => setVerify(e.target.checked)}
|
||||
id="new-scan-no-verify"
|
||||
checked={noVerify}
|
||||
onChange={(e) => setNoVerify(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="new-scan-verify">
|
||||
Build a harness and try to fire each finding's payload in a
|
||||
sandbox.
|
||||
<label htmlFor="new-scan-no-verify">
|
||||
Skip dynamic verification for this scan.
|
||||
</label>
|
||||
</div>
|
||||
<span className="form-hint">
|
||||
Opt-in for now; will become the default once calibrated. Adds
|
||||
wall-clock time per finding.
|
||||
Verification runs by default on Medium and High confidence
|
||||
findings. Check to skip and get a fast static-only result.
|
||||
</span>
|
||||
</div>
|
||||
<div className="scan-modal-actions">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
CompareResponse,
|
||||
ComparedFinding,
|
||||
ChangedFinding,
|
||||
VerdictTransition,
|
||||
} from '../api/types';
|
||||
|
||||
function truncPath(p?: string, max = 50): string {
|
||||
|
|
@ -273,7 +274,104 @@ function CompareByGroup({
|
|||
|
||||
// ── Page ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
type CompareTab = 'status' | 'rule' | 'file';
|
||||
// ── Verdict Diff Tab ─────────────────────────────────────────────────────────
|
||||
|
||||
const TRANSITION_ORDER: VerdictTransition[] = [
|
||||
'FlippedConfirmed',
|
||||
'Regressed',
|
||||
'New',
|
||||
'FlippedNotConfirmed',
|
||||
'Resolved',
|
||||
'Unchanged',
|
||||
];
|
||||
|
||||
const TRANSITION_LABELS: Record<VerdictTransition, string> = {
|
||||
FlippedConfirmed: 'Flipped Confirmed',
|
||||
Regressed: 'Regressed',
|
||||
New: 'New',
|
||||
FlippedNotConfirmed: 'Flipped Not Confirmed',
|
||||
Resolved: 'Resolved',
|
||||
Unchanged: 'Unchanged',
|
||||
};
|
||||
|
||||
const TRANSITION_ROW_CLS: Record<VerdictTransition, string> = {
|
||||
FlippedConfirmed: 'compare-finding-row--new',
|
||||
Regressed: 'compare-finding-row--new',
|
||||
New: 'compare-finding-row--new',
|
||||
FlippedNotConfirmed: 'compare-finding-row--changed',
|
||||
Resolved: 'compare-finding-row--fixed',
|
||||
Unchanged: 'compare-finding-row--unchanged',
|
||||
};
|
||||
|
||||
function VerdictDiffSection({ data }: { data: CompareResponse }) {
|
||||
const entries = data.verdict_diff;
|
||||
if (!entries || entries.length === 0) {
|
||||
return (
|
||||
<div style={{ color: 'var(--text-secondary)', padding: 'var(--space-4)' }}>
|
||||
No verdict-level transitions. Both scans share no findings with stable hashes.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const grouped: Partial<Record<VerdictTransition, typeof entries>> = {};
|
||||
for (const e of entries) {
|
||||
if (!grouped[e.transition]) grouped[e.transition] = [];
|
||||
grouped[e.transition]!.push(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{TRANSITION_ORDER.map((t) => {
|
||||
const items = grouped[t];
|
||||
if (!items || items.length === 0) return null;
|
||||
return (
|
||||
<CollapsibleSection
|
||||
key={t}
|
||||
sectionKey={t}
|
||||
defaultCollapsed={t === 'Unchanged'}
|
||||
headerContent={
|
||||
<>
|
||||
<span
|
||||
className={`compare-finding-row ${TRANSITION_ROW_CLS[t]}`}
|
||||
style={{ padding: '0 var(--space-2)', borderRadius: 'var(--radius-sm)' }}
|
||||
>
|
||||
{TRANSITION_LABELS[t]}
|
||||
</span>
|
||||
<span style={{ marginLeft: 'var(--space-2)' }}>({items.length})</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{items.map((e, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`compare-finding-row ${TRANSITION_ROW_CLS[t]}`}
|
||||
style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)' }}
|
||||
>
|
||||
<span style={{ color: 'var(--text-tertiary)' }}>
|
||||
{e.path}:{e.line}
|
||||
</span>
|
||||
<span>{e.rule_id}</span>
|
||||
{e.baseline_status && (
|
||||
<span style={{ color: 'var(--text-secondary)' }}>
|
||||
{e.baseline_status}
|
||||
</span>
|
||||
)}
|
||||
{e.current_status && (
|
||||
<>
|
||||
<span className="delta-arrow">→</span>
|
||||
<span>{e.current_status}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</CollapsibleSection>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type CompareTab = 'status' | 'rule' | 'file' | 'verdict';
|
||||
|
||||
export function ScanComparePage() {
|
||||
usePageTitle('Compare scans');
|
||||
|
|
@ -403,6 +501,12 @@ export function ScanComparePage() {
|
|||
>
|
||||
By File
|
||||
</button>
|
||||
<button
|
||||
className={`scan-detail-tab ${activeTab === 'verdict' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('verdict')}
|
||||
>
|
||||
Verdict Diff
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="compare-tab-content">
|
||||
|
|
@ -413,6 +517,7 @@ export function ScanComparePage() {
|
|||
{activeTab === 'file' && (
|
||||
<CompareByGroup data={data} groupField="path" />
|
||||
)}
|
||||
{activeTab === 'verdict' && <VerdictDiffSection data={data} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue