@guilz-dev/belay 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/adapters/cursor/runtime-entry.js +1 -0
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +12 -16
- package/dist/adapters/shared/gate-runtime.js +499 -43
- package/dist/bundle/claude-runtime.mjs +8004 -2676
- package/dist/bundle/codex-runtime.mjs +8021 -2683
- package/dist/bundle/cursor-runtime.mjs +7993 -2667
- package/dist/cli.js +180 -6
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +45 -26
- package/dist/commands/classify-for-report.js +1 -1
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +264 -58
- package/dist/commands/doctor.js +47 -2
- package/dist/commands/explain.js +17 -2
- package/dist/commands/harvest.d.ts +29 -0
- package/dist/commands/harvest.js +72 -0
- package/dist/commands/judge.d.ts +11 -1
- package/dist/commands/judge.js +24 -4
- package/dist/commands/metrics.js +34 -2
- package/dist/commands/quality.d.ts +38 -0
- package/dist/commands/quality.js +105 -0
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/commands/simulate.d.ts +1 -0
- package/dist/commands/simulate.js +7 -2
- package/dist/commands/standing-allow.d.ts +10 -0
- package/dist/commands/standing-allow.js +26 -0
- package/dist/commands/tui.d.ts +48 -0
- package/dist/commands/tui.js +150 -0
- package/dist/config-io.js +59 -17
- package/dist/conformance/guarantee-posture.d.ts +21 -0
- package/dist/conformance/guarantee-posture.js +46 -0
- package/dist/conformance/guarantee-table.d.ts +1 -0
- package/dist/conformance/guarantee-table.js +14 -9
- package/dist/core/approval-replay-cli.d.ts +7 -0
- package/dist/core/approval-replay-cli.js +36 -0
- package/dist/core/approval-replay.d.ts +45 -0
- package/dist/core/approval-replay.js +141 -0
- package/dist/core/approval-service.d.ts +15 -0
- package/dist/core/approval-service.js +53 -4
- package/dist/core/approval.d.ts +26 -3
- package/dist/core/approval.js +61 -7
- package/dist/core/audit-analysis.d.ts +7 -1
- package/dist/core/audit-analysis.js +111 -0
- package/dist/core/audit-metrics.d.ts +7 -0
- package/dist/core/audit-metrics.js +24 -4
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +3 -0
- package/dist/core/audit-replay-context.d.ts +35 -0
- package/dist/core/audit-replay-context.js +88 -0
- package/dist/core/audit-types.d.ts +24 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/capability/approval-state-mutation.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.js +92 -0
- package/dist/core/capability/approval-v3.d.ts +15 -0
- package/dist/core/capability/approval-v3.js +74 -0
- package/dist/core/capability/attestation.d.ts +13 -0
- package/dist/core/capability/attestation.js +55 -0
- package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
- package/dist/core/capability/boundary-attestation-sign.js +84 -0
- package/dist/core/capability/boundary-driver-container.d.ts +7 -0
- package/dist/core/capability/boundary-driver-container.js +122 -0
- package/dist/core/capability/boundary-driver.d.ts +29 -0
- package/dist/core/capability/boundary-driver.js +43 -0
- package/dist/core/capability/boundary-egress.d.ts +21 -0
- package/dist/core/capability/boundary-egress.js +109 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +13 -0
- package/dist/core/capability/boundary-grant-materialize.js +78 -0
- package/dist/core/capability/boundary-profile.d.ts +15 -0
- package/dist/core/capability/boundary-profile.js +25 -0
- package/dist/core/capability/boundary-run.d.ts +23 -0
- package/dist/core/capability/boundary-run.js +23 -0
- package/dist/core/capability/boundary-session.d.ts +57 -0
- package/dist/core/capability/boundary-session.js +129 -0
- package/dist/core/capability/capability-request-hash.d.ts +2 -0
- package/dist/core/capability/capability-request-hash.js +8 -0
- package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
- package/dist/core/capability/gate-latency-ratchet.js +18 -0
- package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
- package/dist/core/capability/gate-policy-shadow.js +108 -0
- package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
- package/dist/core/capability/gate-shadow-audit.js +1 -0
- package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
- package/dist/core/capability/gate-shadow-ratchet.js +81 -0
- package/dist/core/capability/grant-lease.d.ts +9 -0
- package/dist/core/capability/grant-lease.js +46 -0
- package/dist/core/capability/grant-loader.d.ts +12 -0
- package/dist/core/capability/grant-loader.js +23 -0
- package/dist/core/capability/grant-match.d.ts +6 -0
- package/dist/core/capability/grant-match.js +93 -0
- package/dist/core/capability/grant.d.ts +16 -0
- package/dist/core/capability/grant.js +16 -0
- package/dist/core/capability/index.d.ts +10 -1
- package/dist/core/capability/index.js +8 -0
- package/dist/core/capability/limits.d.ts +7 -0
- package/dist/core/capability/limits.js +55 -0
- package/dist/core/capability/paths.d.ts +2 -2
- package/dist/core/capability/paths.js +9 -9
- package/dist/core/capability/policy-bridge.d.ts +18 -0
- package/dist/core/capability/policy-bridge.js +51 -0
- package/dist/core/capability/policy-engine.d.ts +70 -0
- package/dist/core/capability/policy-engine.js +476 -0
- package/dist/core/capability/policy-types.d.ts +20 -0
- package/dist/core/capability/policy-types.js +1 -0
- package/dist/core/capability/request.d.ts +45 -0
- package/dist/core/capability/request.js +1 -0
- package/dist/core/capability/resolver.d.ts +4 -0
- package/dist/core/capability/resolver.js +4 -0
- package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
- package/dist/core/capability/trusted-workspace-roots.js +149 -0
- package/dist/core/capability/types.d.ts +11 -1
- package/dist/core/capability-approval.d.ts +2 -1
- package/dist/core/capability-approval.js +100 -2
- package/dist/core/classify-subagent.d.ts +2 -1
- package/dist/core/classify-subagent.js +40 -22
- package/dist/core/classify-tool.js +157 -64
- package/dist/core/config.d.ts +32 -3
- package/dist/core/config.js +105 -3
- package/dist/core/gate-contract.d.ts +3 -0
- package/dist/core/gate-contract.js +3 -0
- package/dist/core/gate-engine.js +12 -7
- package/dist/core/harvest.d.ts +53 -0
- package/dist/core/harvest.js +276 -0
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.js +6 -2
- package/dist/core/judge-doctor.d.ts +3 -0
- package/dist/core/judge-doctor.js +90 -11
- package/dist/core/judge-fallback-hints.d.ts +13 -0
- package/dist/core/judge-fallback-hints.js +108 -0
- package/dist/core/path-utils.d.ts +12 -0
- package/dist/core/path-utils.js +60 -7
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- package/dist/core/recovery/capability.d.ts +3 -0
- package/dist/core/recovery/capability.js +22 -0
- package/dist/core/recovery/fail-closed.d.ts +6 -0
- package/dist/core/recovery/fail-closed.js +25 -0
- package/dist/core/recovery/index.d.ts +3 -0
- package/dist/core/recovery/index.js +2 -0
- package/dist/core/recovery/types.d.ts +20 -0
- package/dist/core/recovery/types.js +1 -0
- package/dist/core/replay-scrub.d.ts +9 -0
- package/dist/core/replay-scrub.js +43 -0
- package/dist/core/shell-tokenizer.d.ts +2 -0
- package/dist/core/shell-tokenizer.js +82 -23
- package/dist/core/standing-allow.d.ts +50 -0
- package/dist/core/standing-allow.js +178 -0
- package/dist/core/transactional/diff-evaluator.js +4 -1
- package/dist/core/transactional/eligibility.js +4 -0
- package/dist/core/transactional/git-worktree.d.ts +10 -3
- package/dist/core/transactional/git-worktree.js +81 -11
- package/dist/core/transactional/reasons.d.ts +1 -0
- package/dist/core/transactional/reasons.js +4 -0
- package/dist/core/transactional/runner.js +22 -6
- package/dist/core/transactional/types.d.ts +4 -0
- package/dist/core/types.d.ts +36 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +19 -6
- package/dist/core/verdict/containment.d.ts +4 -3
- package/dist/core/verdict/containment.js +17 -12
- package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
- package/dist/core/verdict/cursor-acp-client.js +345 -0
- package/dist/core/verdict/egress-classify.d.ts +2 -2
- package/dist/core/verdict/egress-classify.js +2 -2
- package/dist/core/verdict/git-classifier.d.ts +26 -0
- package/dist/core/verdict/git-classifier.js +533 -0
- package/dist/core/verdict/judge-audit.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +33 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +49 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +291 -0
- package/dist/core/verdict/judge-catalog.js +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +6 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +67 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +139 -16
- package/dist/core/verdict/judge-factory.js +13 -4
- package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
- package/dist/core/verdict/judge-provider-matrix.js +66 -0
- package/dist/core/verdict/judge-runtime-config.d.ts +44 -0
- package/dist/core/verdict/judge-runtime-config.js +96 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +200 -0
- package/dist/core/verdict/judge-session-guard.d.ts +27 -0
- package/dist/core/verdict/judge-session-guard.js +91 -0
- package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
- package/dist/core/verdict/judge-session-kill-switch.js +43 -0
- package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
- package/dist/core/verdict/judge-session-mutex.js +23 -0
- package/dist/core/verdict/judge-shadow.d.ts +24 -0
- package/dist/core/verdict/judge-shadow.js +116 -0
- package/dist/core/verdict/judge-transport.d.ts +44 -0
- package/dist/core/verdict/judge-transport.js +268 -0
- package/dist/core/verdict/judge.d.ts +14 -14
- package/dist/core/verdict/judge.js +5 -67
- package/dist/core/verdict/launcher-resolve.d.ts +2 -0
- package/dist/core/verdict/launcher-resolve.js +16 -0
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/prescan.d.ts +15 -0
- package/dist/core/verdict/prescan.js +68 -0
- package/dist/core/verdict/shell-policy.d.ts +25 -0
- package/dist/core/verdict/shell-policy.js +40 -0
- package/dist/core/verdict/shell-semantics.d.ts +24 -0
- package/dist/core/verdict/shell-semantics.js +225 -0
- package/dist/core/verdict/types.d.ts +26 -1
- package/dist/core/verdict/verdict.js +300 -179
- package/dist/corpus/adversarial-probe.d.ts +86 -0
- package/dist/corpus/adversarial-probe.js +220 -0
- package/dist/corpus/evaluate.d.ts +45 -12
- package/dist/corpus/evaluate.js +61 -8
- package/dist/corpus/gate-latency-budget.d.ts +39 -0
- package/dist/corpus/gate-latency-budget.js +33 -0
- package/dist/corpus/gates.d.ts +35 -0
- package/dist/corpus/gates.js +81 -0
- package/dist/corpus/judge-accuracy.d.ts +8 -0
- package/dist/corpus/judge-accuracy.js +39 -0
- package/dist/corpus/must-allow-commands.d.ts +5 -0
- package/dist/corpus/must-allow-commands.js +13 -0
- package/dist/corpus/mutators.d.ts +28 -0
- package/dist/corpus/mutators.js +162 -0
- package/dist/corpus/ratchet.d.ts +42 -0
- package/dist/corpus/ratchet.js +116 -0
- package/dist/corpus/runtime-match.d.ts +19 -0
- package/dist/corpus/runtime-match.js +43 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +99 -0
- package/dist/corpus/types.d.ts +47 -0
- package/dist/corpus/types.js +157 -0
- package/dist/installer.js +4 -1
- package/dist/judge-broker-daemon.d.ts +1 -0
- package/dist/judge-broker-daemon.js +137 -0
- package/dist/services/sandbox-service.d.ts +5 -0
- package/dist/services/sandbox-service.js +30 -2
- package/dist/types.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +7 -1
- package/skills/belay/belay-approve.md +17 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type MutatedCase } from './mutators.js';
|
|
2
|
+
declare const MUST_ASK_VERDICT: "deny_pending_approval";
|
|
3
|
+
export declare function probeCaseKey(entry: {
|
|
4
|
+
core: string;
|
|
5
|
+
mutatorId: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}): string;
|
|
8
|
+
export interface ProbePassedCase {
|
|
9
|
+
core: string;
|
|
10
|
+
mutatorId: string;
|
|
11
|
+
command: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AdversarialProbeFailure {
|
|
14
|
+
core: string;
|
|
15
|
+
mutatorId: string;
|
|
16
|
+
command: string;
|
|
17
|
+
expected: typeof MUST_ASK_VERDICT;
|
|
18
|
+
actual: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AdversarialProbeFpFailure {
|
|
22
|
+
core: string;
|
|
23
|
+
mutatorId: string;
|
|
24
|
+
command: string;
|
|
25
|
+
actual: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
export interface AdversarialProbeReport {
|
|
29
|
+
iteration: number;
|
|
30
|
+
generatedAt: string;
|
|
31
|
+
seed: number;
|
|
32
|
+
batchId: string;
|
|
33
|
+
sourceCommands: string[];
|
|
34
|
+
selectedMutators: string[];
|
|
35
|
+
maxCases?: number;
|
|
36
|
+
holdoutRatio: number;
|
|
37
|
+
totalCases: number;
|
|
38
|
+
fixSetSize: number;
|
|
39
|
+
holdoutSetSize: number;
|
|
40
|
+
fixSetFnRate: number;
|
|
41
|
+
firstPassFnRate: number;
|
|
42
|
+
/** null when no FP probe cases were evaluated. */
|
|
43
|
+
firstPassFpRate: number | null;
|
|
44
|
+
fpProbeSize: number;
|
|
45
|
+
/** null when holdout set is empty, or fix set FN rate is 0 but holdout FN > 0. */
|
|
46
|
+
holdoutFnRate: number | null;
|
|
47
|
+
/** holdoutFnRate / fixSetFnRate; 1.0 when both are 0. null when holdout empty. */
|
|
48
|
+
holdoutFixFnRateRatio: number | null;
|
|
49
|
+
passedCases: ProbePassedCase[];
|
|
50
|
+
failures: AdversarialProbeFailure[];
|
|
51
|
+
fpFailures: AdversarialProbeFpFailure[];
|
|
52
|
+
/** Populated by fix sessions / CI integration; empty when probe-only. */
|
|
53
|
+
filesChanged: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface AdversarialProbeOptions {
|
|
56
|
+
seed?: number;
|
|
57
|
+
holdoutRatio?: number;
|
|
58
|
+
maxCases?: number;
|
|
59
|
+
outputDir?: string;
|
|
60
|
+
repoRoot?: string;
|
|
61
|
+
iteration?: number;
|
|
62
|
+
}
|
|
63
|
+
/** Mulberry32 PRNG for reproducible shuffles. */
|
|
64
|
+
export declare function createSeededRandom(seed: number): () => number;
|
|
65
|
+
export declare function shuffleWithSeed<T>(items: readonly T[], seed: number): T[];
|
|
66
|
+
export declare function splitFixHoldout<T>(items: readonly T[], seed: number, holdoutRatio?: number): {
|
|
67
|
+
fix: T[];
|
|
68
|
+
holdout: T[];
|
|
69
|
+
};
|
|
70
|
+
export declare function generateProbeCases(seed: number, maxCases?: number): MutatedCase[];
|
|
71
|
+
/** Benign must-allow cores × AUTO_LABEL mutators — FP over-stop probe (not for corpus auto-add). */
|
|
72
|
+
export declare function generateFpProbeCases(seed: number): MutatedCase[];
|
|
73
|
+
/** Ratio for overfitting detection; null when holdout unavailable or fix-perfect but holdout not. */
|
|
74
|
+
export declare function holdoutFixFnRateRatio(fixSetFnRate: number, holdoutFnRate: number | null): number | null;
|
|
75
|
+
export declare function runAdversarialProbe(options?: AdversarialProbeOptions): Promise<AdversarialProbeReport>;
|
|
76
|
+
export declare function defaultProbeOutputDir(): string;
|
|
77
|
+
export declare function formatProbeSummary(report: AdversarialProbeReport): string;
|
|
78
|
+
export declare function parseProbeCliArgs(argv: string[]): {
|
|
79
|
+
seed: number;
|
|
80
|
+
holdoutRatio: number;
|
|
81
|
+
maxCases?: number;
|
|
82
|
+
strict: boolean;
|
|
83
|
+
outputDir: string;
|
|
84
|
+
};
|
|
85
|
+
export declare function main(argv?: string[]): Promise<number>;
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { evaluateCommandBatch, evaluateFpCommandBatch } from './evaluate.js';
|
|
5
|
+
import { MUST_ALLOW_SHELL_COMMANDS } from './must-allow-commands.js';
|
|
6
|
+
import { AUTO_LABEL_MUTATORS, CATASTROPHIC_CORES, generateMutatedCases, } from './mutators.js';
|
|
7
|
+
const MUST_ASK_VERDICT = 'deny_pending_approval';
|
|
8
|
+
export function probeCaseKey(entry) {
|
|
9
|
+
return `${entry.core}\0${entry.mutatorId}\0${entry.command}`;
|
|
10
|
+
}
|
|
11
|
+
/** Mulberry32 PRNG for reproducible shuffles. */
|
|
12
|
+
export function createSeededRandom(seed) {
|
|
13
|
+
let state = seed >>> 0;
|
|
14
|
+
return () => {
|
|
15
|
+
state += 0x6d2b79f5;
|
|
16
|
+
let t = state;
|
|
17
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
18
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
19
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function shuffleWithSeed(items, seed) {
|
|
23
|
+
const random = createSeededRandom(seed);
|
|
24
|
+
const copy = [...items];
|
|
25
|
+
for (let i = copy.length - 1; i > 0; i -= 1) {
|
|
26
|
+
const j = Math.floor(random() * (i + 1));
|
|
27
|
+
[copy[i], copy[j]] = [copy[j], copy[i]];
|
|
28
|
+
}
|
|
29
|
+
return copy;
|
|
30
|
+
}
|
|
31
|
+
export function splitFixHoldout(items, seed, holdoutRatio = 0.2) {
|
|
32
|
+
const shuffled = shuffleWithSeed(items, seed);
|
|
33
|
+
const holdoutSize = Math.max(1, Math.floor(shuffled.length * holdoutRatio));
|
|
34
|
+
if (shuffled.length <= 1) {
|
|
35
|
+
return { fix: [...shuffled], holdout: [] };
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
fix: shuffled.slice(holdoutSize),
|
|
39
|
+
holdout: shuffled.slice(0, holdoutSize),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function generateProbeCases(seed, maxCases) {
|
|
43
|
+
const all = generateMutatedCases(CATASTROPHIC_CORES, AUTO_LABEL_MUTATORS);
|
|
44
|
+
const shuffled = shuffleWithSeed(all, seed);
|
|
45
|
+
if (maxCases === undefined || maxCases >= shuffled.length) {
|
|
46
|
+
return shuffled;
|
|
47
|
+
}
|
|
48
|
+
return shuffled.slice(0, maxCases);
|
|
49
|
+
}
|
|
50
|
+
/** Benign must-allow cores × AUTO_LABEL mutators — FP over-stop probe (not for corpus auto-add). */
|
|
51
|
+
export function generateFpProbeCases(seed) {
|
|
52
|
+
const all = generateMutatedCases([...MUST_ALLOW_SHELL_COMMANDS], AUTO_LABEL_MUTATORS);
|
|
53
|
+
return shuffleWithSeed(all, seed + 1000);
|
|
54
|
+
}
|
|
55
|
+
function fnRate(fnCount, total) {
|
|
56
|
+
return total === 0 ? 0 : fnCount / total;
|
|
57
|
+
}
|
|
58
|
+
/** Ratio for overfitting detection; null when holdout unavailable or fix-perfect but holdout not. */
|
|
59
|
+
export function holdoutFixFnRateRatio(fixSetFnRate, holdoutFnRate) {
|
|
60
|
+
if (holdoutFnRate === null) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (fixSetFnRate === 0) {
|
|
64
|
+
return holdoutFnRate === 0 ? 1 : null;
|
|
65
|
+
}
|
|
66
|
+
return holdoutFnRate / fixSetFnRate;
|
|
67
|
+
}
|
|
68
|
+
function toBatchId(date = new Date()) {
|
|
69
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
70
|
+
return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}-${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}`;
|
|
71
|
+
}
|
|
72
|
+
export async function runAdversarialProbe(options = {}) {
|
|
73
|
+
const seed = options.seed ?? 42;
|
|
74
|
+
const holdoutRatio = options.holdoutRatio ?? 0.2;
|
|
75
|
+
const cases = generateProbeCases(seed, options.maxCases);
|
|
76
|
+
const { fix, holdout } = splitFixHoldout(cases, seed + 1, holdoutRatio);
|
|
77
|
+
const toInput = (c) => ({
|
|
78
|
+
command: c.command,
|
|
79
|
+
expected: MUST_ASK_VERDICT,
|
|
80
|
+
core: c.core,
|
|
81
|
+
mutatorId: c.mutatorId,
|
|
82
|
+
});
|
|
83
|
+
const allResults = await evaluateCommandBatch(cases.map(toInput), options.repoRoot);
|
|
84
|
+
const resultByKey = new Map(allResults.map((result) => [
|
|
85
|
+
probeCaseKey({
|
|
86
|
+
core: result.core ?? '',
|
|
87
|
+
mutatorId: result.mutatorId ?? '',
|
|
88
|
+
command: result.command,
|
|
89
|
+
}),
|
|
90
|
+
result,
|
|
91
|
+
]));
|
|
92
|
+
const holdoutResults = holdout
|
|
93
|
+
.map((c) => resultByKey.get(probeCaseKey(c)))
|
|
94
|
+
.filter((r) => r !== undefined);
|
|
95
|
+
const fixResults = fix
|
|
96
|
+
.map((c) => resultByKey.get(probeCaseKey(c)))
|
|
97
|
+
.filter((r) => r !== undefined);
|
|
98
|
+
const passedCases = allResults
|
|
99
|
+
.filter((r) => !r.isFn)
|
|
100
|
+
.map((r) => ({
|
|
101
|
+
core: r.core ?? '',
|
|
102
|
+
mutatorId: r.mutatorId ?? '',
|
|
103
|
+
command: r.command,
|
|
104
|
+
}));
|
|
105
|
+
const failures = allResults
|
|
106
|
+
.filter((r) => r.isFn)
|
|
107
|
+
.map((r) => ({
|
|
108
|
+
core: r.core ?? '',
|
|
109
|
+
mutatorId: r.mutatorId ?? '',
|
|
110
|
+
command: r.command,
|
|
111
|
+
expected: MUST_ASK_VERDICT,
|
|
112
|
+
actual: r.actual,
|
|
113
|
+
reason: r.reason,
|
|
114
|
+
}));
|
|
115
|
+
const fpCases = generateFpProbeCases(seed);
|
|
116
|
+
const fpResults = await evaluateFpCommandBatch(fpCases.map((c) => ({ command: c.command, core: c.core, mutatorId: c.mutatorId })), options.repoRoot);
|
|
117
|
+
const fpFailures = fpResults
|
|
118
|
+
.filter((r) => r.isFp)
|
|
119
|
+
.map((r) => ({
|
|
120
|
+
core: r.core,
|
|
121
|
+
mutatorId: r.mutatorId,
|
|
122
|
+
command: r.command,
|
|
123
|
+
actual: r.actual,
|
|
124
|
+
reason: r.reason,
|
|
125
|
+
}));
|
|
126
|
+
const fixSetFnRate = fnRate(fixResults.filter((r) => r.isFn).length, fixResults.length);
|
|
127
|
+
const holdoutFnRate = holdoutResults.length === 0
|
|
128
|
+
? null
|
|
129
|
+
: fnRate(holdoutResults.filter((r) => r.isFn).length, holdoutResults.length);
|
|
130
|
+
const report = {
|
|
131
|
+
iteration: options.iteration ?? 1,
|
|
132
|
+
generatedAt: new Date().toISOString(),
|
|
133
|
+
seed,
|
|
134
|
+
batchId: toBatchId(),
|
|
135
|
+
sourceCommands: [...CATASTROPHIC_CORES],
|
|
136
|
+
selectedMutators: AUTO_LABEL_MUTATORS.map((m) => m.id),
|
|
137
|
+
...(options.maxCases !== undefined ? { maxCases: options.maxCases } : {}),
|
|
138
|
+
holdoutRatio,
|
|
139
|
+
totalCases: cases.length,
|
|
140
|
+
fixSetSize: fix.length,
|
|
141
|
+
holdoutSetSize: holdout.length,
|
|
142
|
+
fixSetFnRate,
|
|
143
|
+
firstPassFnRate: fnRate(allResults.filter((r) => r.isFn).length, allResults.length),
|
|
144
|
+
firstPassFpRate: fpResults.length === 0
|
|
145
|
+
? null
|
|
146
|
+
: fnRate(fpResults.filter((r) => r.isFp).length, fpResults.length),
|
|
147
|
+
fpProbeSize: fpResults.length,
|
|
148
|
+
holdoutFnRate,
|
|
149
|
+
holdoutFixFnRateRatio: holdoutFixFnRateRatio(fixSetFnRate, holdoutFnRate),
|
|
150
|
+
passedCases,
|
|
151
|
+
failures,
|
|
152
|
+
fpFailures,
|
|
153
|
+
filesChanged: [],
|
|
154
|
+
};
|
|
155
|
+
if (options.outputDir) {
|
|
156
|
+
await mkdir(options.outputDir, { recursive: true });
|
|
157
|
+
const filePath = path.join(options.outputDir, `iteration-${report.batchId}.json`);
|
|
158
|
+
await writeFile(filePath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
159
|
+
}
|
|
160
|
+
return report;
|
|
161
|
+
}
|
|
162
|
+
export function defaultProbeOutputDir() {
|
|
163
|
+
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
164
|
+
return path.join(root, 'artifacts', 'quality-loop');
|
|
165
|
+
}
|
|
166
|
+
export function formatProbeSummary(report) {
|
|
167
|
+
const lines = [
|
|
168
|
+
'Adversarial probe (must-ask mutations)',
|
|
169
|
+
` batchId: ${report.batchId}`,
|
|
170
|
+
` seed: ${report.seed}`,
|
|
171
|
+
` cases: fix=${report.fixSetSize} holdout=${report.holdoutSetSize}`,
|
|
172
|
+
` fixSetFnRate: ${(report.fixSetFnRate * 100).toFixed(1)}%`,
|
|
173
|
+
` firstPassFnRate: ${(report.firstPassFnRate * 100).toFixed(1)}%`,
|
|
174
|
+
` firstPassFpRate: ${report.firstPassFpRate === null ? 'n/a' : `${(report.firstPassFpRate * 100).toFixed(1)}%`}`,
|
|
175
|
+
` holdoutFnRate: ${report.holdoutFnRate === null ? 'n/a' : `${(report.holdoutFnRate * 100).toFixed(1)}%`}`,
|
|
176
|
+
` holdoutFixFnRateRatio: ${report.holdoutFixFnRateRatio === null ? 'n/a' : report.holdoutFixFnRateRatio.toFixed(2)}`,
|
|
177
|
+
` failures: ${report.failures.length}`,
|
|
178
|
+
` fpFailures: ${report.fpFailures.length}`,
|
|
179
|
+
];
|
|
180
|
+
for (const failure of report.failures.slice(0, 10)) {
|
|
181
|
+
lines.push(` - [${failure.mutatorId}] ${JSON.stringify(failure.command)} expected=${failure.expected} actual=${failure.actual} (${failure.reason})`);
|
|
182
|
+
}
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
185
|
+
export function parseProbeCliArgs(argv) {
|
|
186
|
+
let seed = 42;
|
|
187
|
+
let holdoutRatio = 0.2;
|
|
188
|
+
let maxCases;
|
|
189
|
+
let strict = false;
|
|
190
|
+
let outputDir = defaultProbeOutputDir();
|
|
191
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
192
|
+
const arg = argv[i];
|
|
193
|
+
if (arg === '--seed' && argv[i + 1]) {
|
|
194
|
+
seed = Number.parseInt(argv[++i], 10);
|
|
195
|
+
}
|
|
196
|
+
else if (arg === '--holdout-ratio' && argv[i + 1]) {
|
|
197
|
+
holdoutRatio = Number.parseFloat(argv[++i]);
|
|
198
|
+
}
|
|
199
|
+
else if (arg === '--max-cases' && argv[i + 1]) {
|
|
200
|
+
maxCases = Number.parseInt(argv[++i], 10);
|
|
201
|
+
}
|
|
202
|
+
else if (arg === '--output-dir' && argv[i + 1]) {
|
|
203
|
+
outputDir = argv[++i];
|
|
204
|
+
}
|
|
205
|
+
else if (arg === '--strict') {
|
|
206
|
+
strict = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { seed, holdoutRatio, maxCases, strict, outputDir };
|
|
210
|
+
}
|
|
211
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
212
|
+
const { seed, holdoutRatio, maxCases, strict, outputDir } = parseProbeCliArgs(argv);
|
|
213
|
+
const report = await runAdversarialProbe({ seed, holdoutRatio, maxCases, outputDir });
|
|
214
|
+
console.log(formatProbeSummary(report));
|
|
215
|
+
console.log(`\nWrote ${path.join(outputDir, `iteration-${report.batchId}.json`)}`);
|
|
216
|
+
if (strict && report.failures.length > 0) {
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
@@ -1,24 +1,57 @@
|
|
|
1
1
|
import type { Assessment, HookVerdict } from '../core/types.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
2
|
+
import { type CorpusGateMetrics, type CorpusMismatch } from './gates.js';
|
|
3
|
+
import { type CorpusCase, type CorpusCategory, type CorpusProvenanceCounts } from './types.js';
|
|
4
|
+
export type { CategoryGateResult, CorpusGateMetrics, CorpusMismatch, HardGateLimits, } from './gates.js';
|
|
5
|
+
export { computeCategoryGates, hardGateLimitFailures, isAcceptedBenignMismatch, isMustAskMiss, isProvablyBenignBlock, passesHardGates, ZERO_HARD_GATE_LIMITS, } from './gates.js';
|
|
6
|
+
export { DEFAULT_CORPUS_REPO_ROOT, deriveShellCorpusRuntimeKey, enrichProvablyBenignRuntimeKeys, provablyBenignShellRuntimeKeys, } from './runtime-match.js';
|
|
7
|
+
export type { CorpusActionKind, CorpusCase, CorpusCategory } from './types.js';
|
|
8
|
+
export { CORPUS_ACTION_KINDS, CORPUS_CATEGORIES, CorpusSchemaError, parseCorpusCases, } from './types.js';
|
|
7
9
|
export interface CorpusMetrics {
|
|
8
10
|
total: number;
|
|
9
11
|
correct: number;
|
|
10
12
|
accuracy: number;
|
|
11
13
|
precision: Record<string, number>;
|
|
12
14
|
recall: Record<string, number>;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
/** must-ask cases that were not denied (false negative rate on catastrophic corpus). */
|
|
16
|
+
missRate: number;
|
|
17
|
+
/** provably-benign cases that were not silently allowed (over-stop / false positive rate). */
|
|
18
|
+
benignBlockRate: number;
|
|
19
|
+
gates: CorpusGateMetrics;
|
|
20
|
+
categoryCounts: Record<CorpusCategory, number>;
|
|
21
|
+
provenanceCounts: CorpusProvenanceCounts;
|
|
22
|
+
mismatches: CorpusMismatch[];
|
|
20
23
|
}
|
|
21
24
|
export declare function assessmentsDiverge(predicted: Assessment, observed: Assessment): boolean;
|
|
22
25
|
export declare function loadCorpusCases(corpusDir: string): Promise<CorpusCase[]>;
|
|
23
26
|
export declare function evaluateCorpus(cases: CorpusCase[], repoRoot?: string): Promise<CorpusMetrics>;
|
|
24
27
|
export declare function runCorpusEvaluation(corpusDir?: string): Promise<CorpusMetrics>;
|
|
28
|
+
export interface CommandBatchInput {
|
|
29
|
+
command: string;
|
|
30
|
+
expected: HookVerdict;
|
|
31
|
+
core?: string;
|
|
32
|
+
mutatorId?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface CommandBatchResult {
|
|
35
|
+
command: string;
|
|
36
|
+
expected: HookVerdict;
|
|
37
|
+
actual: HookVerdict;
|
|
38
|
+
reason: string;
|
|
39
|
+
core?: string;
|
|
40
|
+
mutatorId?: string;
|
|
41
|
+
isFn: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare function evaluateCommandBatch(cases: CommandBatchInput[], repoRoot?: string): Promise<CommandBatchResult[]>;
|
|
44
|
+
export interface FpCommandBatchResult {
|
|
45
|
+
command: string;
|
|
46
|
+
actual: HookVerdict;
|
|
47
|
+
reason: string;
|
|
48
|
+
core: string;
|
|
49
|
+
mutatorId: string;
|
|
50
|
+
isFp: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** False-positive probe: must-allow cores that receive deny_pending_approval count as FP. */
|
|
53
|
+
export declare function evaluateFpCommandBatch(cases: Array<{
|
|
54
|
+
command: string;
|
|
55
|
+
core: string;
|
|
56
|
+
mutatorId: string;
|
|
57
|
+
}>, repoRoot?: string): Promise<FpCommandBatchResult[]>;
|
package/dist/corpus/evaluate.js
CHANGED
|
@@ -3,7 +3,12 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { classifierOptionsFromConfig, DEFAULT_CONFIG_V3 } from '../core/config.js';
|
|
5
5
|
import { classifyShell } from '../core/verdict/adapter.js';
|
|
6
|
-
import {
|
|
6
|
+
import { computeCategoryGates, isMustAskMiss, } from './gates.js';
|
|
7
|
+
import { defaultCorpusEvalPaths, enrichProvablyBenignRuntimeKeys } from './runtime-match.js';
|
|
8
|
+
import { countByCategory, countProvenanceBySource, parseCorpusCases, } from './types.js';
|
|
9
|
+
export { computeCategoryGates, hardGateLimitFailures, isAcceptedBenignMismatch, isMustAskMiss, isProvablyBenignBlock, passesHardGates, ZERO_HARD_GATE_LIMITS, } from './gates.js';
|
|
10
|
+
export { DEFAULT_CORPUS_REPO_ROOT, deriveShellCorpusRuntimeKey, enrichProvablyBenignRuntimeKeys, provablyBenignShellRuntimeKeys, } from './runtime-match.js';
|
|
11
|
+
export { CORPUS_ACTION_KINDS, CORPUS_CATEGORIES, CorpusSchemaError, parseCorpusCases, } from './types.js';
|
|
7
12
|
export function assessmentsDiverge(predicted, observed) {
|
|
8
13
|
return (predicted.reversibility !== observed.reversibility ||
|
|
9
14
|
predicted.external !== observed.external ||
|
|
@@ -12,20 +17,22 @@ export function assessmentsDiverge(predicted, observed) {
|
|
|
12
17
|
const VERDICTS = ['allow', 'allow_flagged', 'deny_pending_approval'];
|
|
13
18
|
export async function loadCorpusCases(corpusDir) {
|
|
14
19
|
const raw = await readFile(path.join(corpusDir, 'shell-commands.json'), 'utf8');
|
|
15
|
-
|
|
20
|
+
const cases = parseCorpusCases(JSON.parse(raw));
|
|
21
|
+
return enrichProvablyBenignRuntimeKeys(cases);
|
|
16
22
|
}
|
|
17
|
-
export async function evaluateCorpus(cases, repoRoot =
|
|
23
|
+
export async function evaluateCorpus(cases, repoRoot = defaultCorpusEvalPaths().repoRoot) {
|
|
18
24
|
const cwd = path.join(repoRoot, 'src');
|
|
19
25
|
const options = classifierOptionsFromConfig(DEFAULT_CONFIG_V3);
|
|
20
26
|
const mismatches = [];
|
|
27
|
+
const results = [];
|
|
21
28
|
let correct = 0;
|
|
22
29
|
const confusion = {};
|
|
23
30
|
for (const expected of VERDICTS) {
|
|
24
31
|
confusion[expected] = { allow: 0, allow_flagged: 0, deny_pending_approval: 0 };
|
|
25
32
|
}
|
|
26
|
-
const judge = createDeterministicJudgeStub();
|
|
27
33
|
for (const testCase of cases) {
|
|
28
|
-
const result = await classifyShell(testCase.command, cwd, repoRoot, DEFAULT_CONFIG_V3, options
|
|
34
|
+
const result = await classifyShell(testCase.command, cwd, repoRoot, DEFAULT_CONFIG_V3, options);
|
|
35
|
+
results.push({ actual: result.verdict, reason: result.reason });
|
|
29
36
|
confusion[testCase.verdict][result.verdict] += 1;
|
|
30
37
|
const verdictOk = result.verdict === testCase.verdict;
|
|
31
38
|
const reasonOk = !testCase.reason || result.reason === testCase.reason;
|
|
@@ -35,6 +42,7 @@ export async function evaluateCorpus(cases, repoRoot = '/workspace/project') {
|
|
|
35
42
|
else {
|
|
36
43
|
mismatches.push({
|
|
37
44
|
command: testCase.command,
|
|
45
|
+
category: testCase.category,
|
|
38
46
|
expected: testCase.verdict,
|
|
39
47
|
actual: result.verdict,
|
|
40
48
|
reason: result.reason,
|
|
@@ -50,15 +58,18 @@ export async function evaluateCorpus(cases, repoRoot = '/workspace/project') {
|
|
|
50
58
|
const actualTotal = VERDICTS.reduce((sum, key) => sum + confusion[verdict][key], 0);
|
|
51
59
|
recall[verdict] = actualTotal === 0 ? 1 : truePositive / actualTotal;
|
|
52
60
|
}
|
|
53
|
-
const
|
|
54
|
-
const denyCases = cases.filter((entry) => entry.verdict === 'deny_pending_approval').length;
|
|
61
|
+
const gates = computeCategoryGates(cases, results);
|
|
55
62
|
return {
|
|
56
63
|
total: cases.length,
|
|
57
64
|
correct,
|
|
58
65
|
accuracy: cases.length === 0 ? 1 : correct / cases.length,
|
|
59
66
|
precision,
|
|
60
67
|
recall,
|
|
61
|
-
|
|
68
|
+
missRate: gates.mustAsk.rate,
|
|
69
|
+
benignBlockRate: gates.provablyBenign.rate,
|
|
70
|
+
gates,
|
|
71
|
+
categoryCounts: countByCategory(cases),
|
|
72
|
+
provenanceCounts: countProvenanceBySource(cases),
|
|
62
73
|
mismatches,
|
|
63
74
|
};
|
|
64
75
|
}
|
|
@@ -67,3 +78,45 @@ export async function runCorpusEvaluation(corpusDir) {
|
|
|
67
78
|
const cases = await loadCorpusCases(root);
|
|
68
79
|
return evaluateCorpus(cases);
|
|
69
80
|
}
|
|
81
|
+
export async function evaluateCommandBatch(cases, repoRoot = defaultCorpusEvalPaths().repoRoot) {
|
|
82
|
+
const { cwd } = defaultCorpusEvalPaths(repoRoot);
|
|
83
|
+
const options = classifierOptionsFromConfig(DEFAULT_CONFIG_V3);
|
|
84
|
+
const results = [];
|
|
85
|
+
for (const testCase of cases) {
|
|
86
|
+
const result = await classifyShell(testCase.command, cwd, repoRoot, DEFAULT_CONFIG_V3, options);
|
|
87
|
+
const isFn = isMustAskMiss({
|
|
88
|
+
category: 'must-ask',
|
|
89
|
+
command: testCase.command,
|
|
90
|
+
verdict: testCase.expected,
|
|
91
|
+
kind: 'shell',
|
|
92
|
+
}, result.verdict);
|
|
93
|
+
results.push({
|
|
94
|
+
command: testCase.command,
|
|
95
|
+
expected: testCase.expected,
|
|
96
|
+
actual: result.verdict,
|
|
97
|
+
reason: result.reason,
|
|
98
|
+
core: testCase.core,
|
|
99
|
+
mutatorId: testCase.mutatorId,
|
|
100
|
+
isFn,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
105
|
+
/** False-positive probe: must-allow cores that receive deny_pending_approval count as FP. */
|
|
106
|
+
export async function evaluateFpCommandBatch(cases, repoRoot = defaultCorpusEvalPaths().repoRoot) {
|
|
107
|
+
const { cwd } = defaultCorpusEvalPaths(repoRoot);
|
|
108
|
+
const options = classifierOptionsFromConfig(DEFAULT_CONFIG_V3);
|
|
109
|
+
const results = [];
|
|
110
|
+
for (const testCase of cases) {
|
|
111
|
+
const result = await classifyShell(testCase.command, cwd, repoRoot, DEFAULT_CONFIG_V3, options);
|
|
112
|
+
results.push({
|
|
113
|
+
command: testCase.command,
|
|
114
|
+
actual: result.verdict,
|
|
115
|
+
reason: result.reason,
|
|
116
|
+
core: testCase.core,
|
|
117
|
+
mutatorId: testCase.mutatorId,
|
|
118
|
+
isFp: result.verdict === 'deny_pending_approval',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate sync classification latency budgets (measured baselines + CI thresholds).
|
|
3
|
+
* PLAN target: p95 <= 100ms, max <= 500ms for full corpus — ratchet toward these over time.
|
|
4
|
+
*/
|
|
5
|
+
/** Warm local corpus run (2026-08-06). Step 3 uses max(measured × 1.2, Step 1 floor); floors dominate until re-measured and lowered. */
|
|
6
|
+
export declare const GATE_LATENCY_MEASURED_BASELINE: {
|
|
7
|
+
readonly shell: {
|
|
8
|
+
readonly p95Ms: 1;
|
|
9
|
+
readonly maxMs: 1;
|
|
10
|
+
};
|
|
11
|
+
readonly tool: {
|
|
12
|
+
readonly p95Ms: 1;
|
|
13
|
+
readonly maxMs: 1;
|
|
14
|
+
};
|
|
15
|
+
readonly subagent: {
|
|
16
|
+
readonly p95Ms: 7;
|
|
17
|
+
readonly maxMs: 7;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/** Step 3 CI thresholds: max(measured p95 × 1.2, Step 1 floor). */
|
|
21
|
+
export declare const GATE_LATENCY_BUDGET: {
|
|
22
|
+
readonly shell: {
|
|
23
|
+
p95Ms: number;
|
|
24
|
+
maxMs: number;
|
|
25
|
+
};
|
|
26
|
+
readonly tool: {
|
|
27
|
+
p95Ms: number;
|
|
28
|
+
maxMs: number;
|
|
29
|
+
};
|
|
30
|
+
readonly subagent: {
|
|
31
|
+
p95Ms: number;
|
|
32
|
+
maxMs: number;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/** PLAN acceptance targets (quality-loop ratchet goal). */
|
|
36
|
+
export declare const GATE_LATENCY_PLAN_TARGET: {
|
|
37
|
+
readonly p95Ms: 100;
|
|
38
|
+
readonly maxMs: 500;
|
|
39
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate sync classification latency budgets (measured baselines + CI thresholds).
|
|
3
|
+
* PLAN target: p95 <= 100ms, max <= 500ms for full corpus — ratchet toward these over time.
|
|
4
|
+
*/
|
|
5
|
+
/** Warm local corpus run (2026-08-06). Step 3 uses max(measured × 1.2, Step 1 floor); floors dominate until re-measured and lowered. */
|
|
6
|
+
export const GATE_LATENCY_MEASURED_BASELINE = {
|
|
7
|
+
shell: { p95Ms: 1, maxMs: 1 },
|
|
8
|
+
tool: { p95Ms: 1, maxMs: 1 },
|
|
9
|
+
subagent: { p95Ms: 7, maxMs: 7 },
|
|
10
|
+
};
|
|
11
|
+
/** Step 1 rollout floors — CI thresholds stay at these until measured × 1.2 exceeds them. */
|
|
12
|
+
const GATE_LATENCY_STEP1_FLOOR = {
|
|
13
|
+
shell: { p95Ms: 200, maxMs: 1000 },
|
|
14
|
+
tool: { p95Ms: 300, maxMs: 1500 },
|
|
15
|
+
subagent: { p95Ms: 200, maxMs: 1000 },
|
|
16
|
+
};
|
|
17
|
+
function ciBudgetFromBaseline(baseline, floor) {
|
|
18
|
+
return {
|
|
19
|
+
p95Ms: Math.max(floor.p95Ms, Math.ceil(baseline.p95Ms * 1.2)),
|
|
20
|
+
maxMs: Math.max(floor.maxMs, Math.ceil(baseline.maxMs * 1.2)),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Step 3 CI thresholds: max(measured p95 × 1.2, Step 1 floor). */
|
|
24
|
+
export const GATE_LATENCY_BUDGET = {
|
|
25
|
+
shell: ciBudgetFromBaseline(GATE_LATENCY_MEASURED_BASELINE.shell, GATE_LATENCY_STEP1_FLOOR.shell),
|
|
26
|
+
tool: ciBudgetFromBaseline(GATE_LATENCY_MEASURED_BASELINE.tool, GATE_LATENCY_STEP1_FLOOR.tool),
|
|
27
|
+
subagent: ciBudgetFromBaseline(GATE_LATENCY_MEASURED_BASELINE.subagent, GATE_LATENCY_STEP1_FLOOR.subagent),
|
|
28
|
+
};
|
|
29
|
+
/** PLAN acceptance targets (quality-loop ratchet goal). */
|
|
30
|
+
export const GATE_LATENCY_PLAN_TARGET = {
|
|
31
|
+
p95Ms: 100,
|
|
32
|
+
maxMs: 500,
|
|
33
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { HookVerdict } from '../core/types.js';
|
|
2
|
+
import { type CorpusCase, type CorpusCategory } from './types.js';
|
|
3
|
+
export interface CorpusMismatch {
|
|
4
|
+
command: string;
|
|
5
|
+
category: CorpusCategory;
|
|
6
|
+
expected: HookVerdict;
|
|
7
|
+
actual: HookVerdict;
|
|
8
|
+
reason: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CategoryGateResult {
|
|
11
|
+
total: number;
|
|
12
|
+
mismatches: number;
|
|
13
|
+
rate: number;
|
|
14
|
+
}
|
|
15
|
+
export interface CorpusGateMetrics {
|
|
16
|
+
mustAsk: CategoryGateResult;
|
|
17
|
+
provablyBenign: CategoryGateResult;
|
|
18
|
+
acceptedBenign: CategoryGateResult;
|
|
19
|
+
}
|
|
20
|
+
export interface HardGateLimits {
|
|
21
|
+
mustAskMisses: number;
|
|
22
|
+
provablyBenignBlocks: number;
|
|
23
|
+
}
|
|
24
|
+
export declare const ZERO_HARD_GATE_LIMITS: HardGateLimits;
|
|
25
|
+
/** Verdict-level: must-ask requires deny; reason mismatches are not hard-gate failures. */
|
|
26
|
+
export declare function isMustAskMiss(testCase: CorpusCase, actual: HookVerdict): boolean;
|
|
27
|
+
/** Any non-allow verdict (including allow_flagged) is an over-stop on provably-benign. */
|
|
28
|
+
export declare function isProvablyBenignBlock(testCase: CorpusCase, actual: HookVerdict): boolean;
|
|
29
|
+
export declare function isAcceptedBenignMismatch(testCase: CorpusCase, actual: HookVerdict, actualReason: string): boolean;
|
|
30
|
+
export declare function computeCategoryGates(cases: CorpusCase[], results: Array<{
|
|
31
|
+
actual: HookVerdict;
|
|
32
|
+
reason: string;
|
|
33
|
+
}>): CorpusGateMetrics;
|
|
34
|
+
export declare function passesHardGates(gates: CorpusGateMetrics, limits?: Partial<HardGateLimits>): boolean;
|
|
35
|
+
export declare function hardGateLimitFailures(gates: CorpusGateMetrics, limits?: Partial<HardGateLimits>): Array<'must-ask' | 'provably-benign'>;
|