@guilz-dev/belay 0.2.0 → 0.4.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 +39 -12
- package/dist/adapters/cursor/runtime-entry.js +1 -0
- package/dist/adapters/shared/gate-runtime.d.ts +11 -14
- package/dist/adapters/shared/gate-runtime.js +289 -23
- package/dist/bundle/claude-runtime.mjs +4435 -708
- package/dist/bundle/codex-runtime.mjs +4448 -710
- package/dist/bundle/cursor-runtime.mjs +4437 -710
- package/dist/cli.js +333 -11
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +56 -26
- package/dist/commands/config.d.ts +70 -0
- package/dist/commands/config.js +577 -0
- package/dist/commands/doctor.js +13 -2
- package/dist/commands/explain.js +6 -1
- package/dist/commands/harvest.d.ts +29 -0
- package/dist/commands/harvest.js +72 -0
- package/dist/commands/health-snapshot.d.ts +6 -0
- package/dist/commands/health-snapshot.js +34 -0
- package/dist/commands/judge.d.ts +100 -0
- package/dist/commands/judge.js +304 -0
- 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/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/status.js +2 -2
- package/dist/commands/stdin-key.d.ts +1 -0
- package/dist/commands/stdin-key.js +8 -0
- package/dist/commands/tui.d.ts +48 -0
- package/dist/commands/tui.js +150 -0
- package/dist/config-io.js +10 -5
- package/dist/conformance/guarantee-table.js +12 -0
- package/dist/conformance/types.d.ts +2 -0
- 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 +47 -2
- package/dist/core/approval.d.ts +24 -3
- package/dist/core/approval.js +47 -4
- package/dist/core/audit-analysis.d.ts +7 -1
- package/dist/core/audit-analysis.js +111 -0
- package/dist/core/audit-io.d.ts +2 -0
- package/dist/core/audit-io.js +14 -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/index.d.ts +4 -3
- package/dist/core/capability/index.js +3 -2
- package/dist/core/capability/paths.d.ts +2 -1
- package/dist/core/capability/paths.js +76 -8
- package/dist/core/capability/reasons.d.ts +3 -0
- package/dist/core/capability/reasons.js +8 -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.js +2 -20
- package/dist/core/classify-tool.js +129 -30
- package/dist/core/config-layers.js +2 -1
- package/dist/core/config.d.ts +40 -2
- package/dist/core/config.js +159 -11
- package/dist/core/credential-store.d.ts +11 -0
- package/dist/core/credential-store.js +60 -0
- package/dist/core/gate-engine.js +104 -13
- 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/integrity.d.ts +2 -0
- package/dist/core/integrity.js +13 -0
- package/dist/core/judge-api-key.d.ts +19 -0
- package/dist/core/judge-api-key.js +74 -0
- package/dist/core/judge-cloud-consent.d.ts +13 -0
- package/dist/core/judge-cloud-consent.js +38 -0
- package/dist/core/judge-config.d.ts +41 -4
- package/dist/core/judge-config.js +263 -57
- package/dist/core/judge-doctor.d.ts +9 -1
- package/dist/core/judge-doctor.js +199 -94
- package/dist/core/judge-model-discovery.d.ts +24 -0
- package/dist/core/judge-model-discovery.js +168 -0
- package/dist/core/judge-model-policy.d.ts +5 -0
- package/dist/core/judge-model-policy.js +21 -0
- package/dist/core/judge-runtime-detection.d.ts +9 -0
- package/dist/core/judge-runtime-detection.js +68 -0
- package/dist/core/path-utils.d.ts +11 -0
- package/dist/core/path-utils.js +56 -6
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- 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 +175 -0
- package/dist/core/transactional/diff-evaluator.js +1 -19
- package/dist/core/types.d.ts +17 -0
- package/dist/core/verdict/adapter.d.ts +1 -0
- package/dist/core/verdict/adapter.js +17 -2
- package/dist/core/verdict/containment.d.ts +8 -2
- package/dist/core/verdict/containment.js +45 -10
- package/dist/core/verdict/judge-audit.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +32 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +48 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +279 -0
- package/dist/core/verdict/judge-catalog.d.ts +40 -0
- package/dist/core/verdict/judge-catalog.js +148 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
- package/dist/core/verdict/judge-cli.d.ts +46 -0
- package/dist/core/verdict/judge-cli.js +401 -0
- package/dist/core/verdict/judge-factory.d.ts +15 -4
- package/dist/core/verdict/judge-factory.js +127 -15
- 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 +42 -0
- package/dist/core/verdict/judge-runtime-config.js +92 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +195 -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 +40 -0
- package/dist/core/verdict/judge-transport.js +258 -0
- package/dist/core/verdict/judge.d.ts +33 -1
- package/dist/core/verdict/judge.js +84 -14
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/persistent-paths.d.ts +8 -0
- package/dist/core/verdict/persistent-paths.js +52 -0
- package/dist/core/verdict/types.d.ts +19 -2
- package/dist/core/verdict/verdict.js +166 -49
- 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 +62 -5
- 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 +151 -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 +45 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +79 -0
- package/dist/corpus/types.d.ts +47 -0
- package/dist/corpus/types.js +157 -0
- package/dist/installer.js +69 -15
- package/dist/judge-broker-daemon.d.ts +1 -0
- package/dist/judge-broker-daemon.js +123 -0
- package/dist/services/sandbox-service.d.ts +1 -0
- package/dist/services/sandbox-service.js +2 -0
- package/dist/types.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +15 -8
- package/skills/belay/belay-approve.md +17 -0
- package/dist/commands/init-wizard.d.ts +0 -21
- package/dist/commands/init-wizard.js +0 -63
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadConfigFile } from '../config-io.js';
|
|
4
|
+
import { parseAuditNdjson } from '../core/audit-metrics.js';
|
|
5
|
+
import { toAuditRecord } from '../core/audit-query.js';
|
|
6
|
+
import { applyHarvestReview, buildHarvestReport, filterRecordsForHarvest, } from '../core/harvest.js';
|
|
7
|
+
import { parseCorpusCases } from '../corpus/types.js';
|
|
8
|
+
import { loadAuditRecords } from './audit.js';
|
|
9
|
+
export async function harvestListProject(options = {}) {
|
|
10
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
11
|
+
const config = await loadConfigFile(repoRoot);
|
|
12
|
+
const records = await loadAuditRecords(repoRoot);
|
|
13
|
+
return harvestReportFromRecords(records, {
|
|
14
|
+
since: options.since,
|
|
15
|
+
until: options.until,
|
|
16
|
+
allowPatterns: config.overrides.allow,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function harvestReportFromRecords(records, options = {}) {
|
|
20
|
+
return buildHarvestReport(filterRecordsForHarvest(records, { since: options.since, until: options.until }), { allowPatterns: options.allowPatterns });
|
|
21
|
+
}
|
|
22
|
+
export function formatHarvestReport(report) {
|
|
23
|
+
const lines = [
|
|
24
|
+
`belay harvest (scope: ${report.scope} audit traces only)`,
|
|
25
|
+
`Schema: v${report.schemaVersion}`,
|
|
26
|
+
'',
|
|
27
|
+
`Benign candidates (${report.candidates.length}):`,
|
|
28
|
+
];
|
|
29
|
+
if (report.candidates.length === 0) {
|
|
30
|
+
lines.push('- (none)');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
for (const candidate of report.candidates) {
|
|
34
|
+
lines.push(`- ${JSON.stringify(candidate.command)} [${candidate.sources.join(', ')}] asks=${candidate.askCount} approved=${candidate.approvedAfterDeny ? 'yes' : 'no'} fp=${candidate.fingerprint.slice(0, 12)}…`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
lines.push('', `Availability queue (${report.availabilityQueue.length}):`);
|
|
38
|
+
if (report.availabilityQueue.length === 0) {
|
|
39
|
+
lines.push('- (none)');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
for (const entry of report.availabilityQueue) {
|
|
43
|
+
lines.push(`- ${JSON.stringify(entry.command)} signal=${entry.availabilitySignal} asks=${entry.askCount} (${entry.reason})`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
lines.push('', 'Candidates are review-only signals — approve in audit does not auto-promote to corpus.', 'Time filters (--since/--until) keep paired deny/approval rows for round-trip detection.', 'Use: belay harvest apply --command "<text>" --outcome provably-benign|accepted-benign|reject');
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
export async function harvestApplyProject(options) {
|
|
50
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
51
|
+
const corpusPath = path.resolve(repoRoot, options.corpusPath ?? path.join('corpus', 'shell-commands.json'));
|
|
52
|
+
const raw = await readFile(corpusPath, 'utf8');
|
|
53
|
+
const cases = parseCorpusCases(JSON.parse(raw));
|
|
54
|
+
const result = applyHarvestReview(cases, {
|
|
55
|
+
command: options.command,
|
|
56
|
+
outcome: options.outcome,
|
|
57
|
+
reason: options.reason,
|
|
58
|
+
});
|
|
59
|
+
if (result.applied) {
|
|
60
|
+
await writeFile(corpusPath, `${JSON.stringify(result.cases, null, 2)}\n`, 'utf8');
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
ok: result.ok,
|
|
64
|
+
message: result.message,
|
|
65
|
+
corpusPath: path.relative(repoRoot, corpusPath) || corpusPath,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Parse audit ndjson for tests without full project layout. */
|
|
69
|
+
export function harvestReportFromNdjson(raw) {
|
|
70
|
+
const records = parseAuditNdjson(raw).map((entry) => toAuditRecord(entry));
|
|
71
|
+
return harvestReportFromRecords(records);
|
|
72
|
+
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
import type { AdapterName } from '../adapters/layouts/types.js';
|
|
1
2
|
import type { HealthSnapshot, HealthSnapshotOptions } from '../types.js';
|
|
3
|
+
/** Lightweight floor check without judge doctor / sandbox probes. */
|
|
4
|
+
export declare function isBelayFloorInstalled(options?: {
|
|
5
|
+
targetDir?: string;
|
|
6
|
+
adapter?: AdapterName;
|
|
7
|
+
}): Promise<boolean>;
|
|
2
8
|
export declare function collectHealthSnapshot(options?: HealthSnapshotOptions): Promise<HealthSnapshot>;
|
|
@@ -48,6 +48,40 @@ async function managedHooksPresent(adapter, hooksPath, hooksDir, repoRoot) {
|
|
|
48
48
|
}
|
|
49
49
|
return managedEntries.every(({ definition }) => content.includes(definition.command));
|
|
50
50
|
}
|
|
51
|
+
/** Lightweight floor check without judge doctor / sandbox probes. */
|
|
52
|
+
export async function isBelayFloorInstalled(options = {}) {
|
|
53
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
54
|
+
const adapter = options.adapter ?? detectAdapterName(repoRoot);
|
|
55
|
+
const layout = getAdapterLayout(adapter);
|
|
56
|
+
const configPath = layout.configPath(repoRoot);
|
|
57
|
+
if (!existsSync(configPath)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
let installScope = 'project';
|
|
61
|
+
try {
|
|
62
|
+
const layered = await loadLayeredConfig(repoRoot, adapter);
|
|
63
|
+
installScope = layered.config.installScope === 'global' ? 'global' : 'project';
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const scopedPaths = resolveScopedPaths(layout, installScope, repoRoot);
|
|
69
|
+
const hooksPath = scopedPaths.hooksSettingsPath;
|
|
70
|
+
const hooksDir = scopedPaths.hooksDir;
|
|
71
|
+
const corePath = path.join(scopedPaths.runtimeDir, 'core.mjs');
|
|
72
|
+
const runtimePresent = existsSync(corePath);
|
|
73
|
+
const runnerPresent = existsSync(path.join(hooksDir, 'belay-runner')) ||
|
|
74
|
+
existsSync(path.join(hooksDir, 'belay-runner.cmd'));
|
|
75
|
+
if (!existsSync(hooksPath) || !runnerPresent || !runtimePresent) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return await managedHooksPresent(adapter, hooksPath, hooksDir, repoRoot);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
51
85
|
export async function collectHealthSnapshot(options = {}) {
|
|
52
86
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
53
87
|
const adapter = options.adapter ?? detectAdapterName(repoRoot);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { BelayJudgeConfig } from '../core/config.js';
|
|
2
|
+
export interface JudgeCommandOptions {
|
|
3
|
+
targetDir?: string;
|
|
4
|
+
json?: boolean;
|
|
5
|
+
subcommand?: 'status' | 'list' | 'use' | 'test' | 'bench' | 'consent';
|
|
6
|
+
liveProbe?: boolean;
|
|
7
|
+
providerId?: string;
|
|
8
|
+
model?: string;
|
|
9
|
+
endpoint?: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
acceptCloud?: boolean;
|
|
12
|
+
cloudConsentApprovalId?: string;
|
|
13
|
+
credentialMode?: 'project' | 'apiKey';
|
|
14
|
+
keyStdin?: boolean;
|
|
15
|
+
keyEnv?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function isInteractiveTTY(): boolean;
|
|
18
|
+
export declare function judgeStatus(options?: JudgeCommandOptions): Promise<string | {
|
|
19
|
+
providerId: import("../core/config.js").JudgeProviderId | import("../core/config.js").DeprecatedJudgeProviderId | undefined;
|
|
20
|
+
provider: import("../core/config.js").JudgeProvider;
|
|
21
|
+
endpoint: string | null;
|
|
22
|
+
model: string;
|
|
23
|
+
modelResolved: string;
|
|
24
|
+
transport: import("../core/verdict/judge.js").Tier1JudgeTransport;
|
|
25
|
+
credential: import("../core/judge-api-key.js").ResolvedJudgeCredential;
|
|
26
|
+
cloudConsent: import("../core/config.js").JudgeCloudConsent | null;
|
|
27
|
+
cloudJudgeActive: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function judgeList(options?: JudgeCommandOptions): string | {
|
|
30
|
+
id: import("../core/verdict/judge-catalog.js").JudgeProviderId;
|
|
31
|
+
driver: import("../core/config.js").JudgeProvider;
|
|
32
|
+
cloud: boolean;
|
|
33
|
+
defaultEndpoint: string | null;
|
|
34
|
+
defaultModel: string;
|
|
35
|
+
apiKeyEnvVars: string[];
|
|
36
|
+
}[];
|
|
37
|
+
export declare function judgeUse(options: JudgeCommandOptions): Promise<string | {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
configPath: string;
|
|
40
|
+
diff: string;
|
|
41
|
+
warnings: string[];
|
|
42
|
+
judge: BelayJudgeConfig;
|
|
43
|
+
}>;
|
|
44
|
+
export declare function judgeRequestCloudConsent(options?: JudgeCommandOptions): Promise<string | {
|
|
45
|
+
approvalId: string;
|
|
46
|
+
created: boolean;
|
|
47
|
+
providerId: string;
|
|
48
|
+
endpoint: string;
|
|
49
|
+
reason: "judge_cloud_consent";
|
|
50
|
+
}>;
|
|
51
|
+
export declare function judgeTest(options?: JudgeCommandOptions): Promise<string | {
|
|
52
|
+
modelCheck: import("../core/judge-model-discovery.js").CheckJudgeModelPresenceResult | undefined;
|
|
53
|
+
issues: string[];
|
|
54
|
+
warnings: string[];
|
|
55
|
+
notes: string[];
|
|
56
|
+
}>;
|
|
57
|
+
export declare function judgeBench(options?: JudgeCommandOptions): Promise<string | {
|
|
58
|
+
slo: typeof import("../core/verdict/judge-runtime-config.js").JUDGE_LATENCY_SLO;
|
|
59
|
+
measured: Record<import("../core/verdict/judge-baseline.js").JudgeLatencyPhase, import("../core/verdict/judge-baseline.js").JudgeLatencyPercentiles>;
|
|
60
|
+
sessionMeetsTarget: boolean;
|
|
61
|
+
}>;
|
|
62
|
+
export declare function runJudgeCommand(options: JudgeCommandOptions): Promise<string | {
|
|
63
|
+
slo: typeof import("../core/verdict/judge-runtime-config.js").JUDGE_LATENCY_SLO;
|
|
64
|
+
measured: Record<import("../core/verdict/judge-baseline.js").JudgeLatencyPhase, import("../core/verdict/judge-baseline.js").JudgeLatencyPercentiles>;
|
|
65
|
+
sessionMeetsTarget: boolean;
|
|
66
|
+
} | {
|
|
67
|
+
providerId: import("../core/config.js").JudgeProviderId | import("../core/config.js").DeprecatedJudgeProviderId | undefined;
|
|
68
|
+
provider: import("../core/config.js").JudgeProvider;
|
|
69
|
+
endpoint: string | null;
|
|
70
|
+
model: string;
|
|
71
|
+
modelResolved: string;
|
|
72
|
+
transport: import("../core/verdict/judge.js").Tier1JudgeTransport;
|
|
73
|
+
credential: import("../core/judge-api-key.js").ResolvedJudgeCredential;
|
|
74
|
+
cloudConsent: import("../core/config.js").JudgeCloudConsent | null;
|
|
75
|
+
cloudJudgeActive: boolean;
|
|
76
|
+
} | {
|
|
77
|
+
id: import("../core/verdict/judge-catalog.js").JudgeProviderId;
|
|
78
|
+
driver: import("../core/config.js").JudgeProvider;
|
|
79
|
+
cloud: boolean;
|
|
80
|
+
defaultEndpoint: string | null;
|
|
81
|
+
defaultModel: string;
|
|
82
|
+
apiKeyEnvVars: string[];
|
|
83
|
+
}[] | {
|
|
84
|
+
ok: boolean;
|
|
85
|
+
configPath: string;
|
|
86
|
+
diff: string;
|
|
87
|
+
warnings: string[];
|
|
88
|
+
judge: BelayJudgeConfig;
|
|
89
|
+
} | {
|
|
90
|
+
approvalId: string;
|
|
91
|
+
created: boolean;
|
|
92
|
+
providerId: string;
|
|
93
|
+
endpoint: string;
|
|
94
|
+
reason: "judge_cloud_consent";
|
|
95
|
+
} | {
|
|
96
|
+
modelCheck: import("../core/judge-model-discovery.js").CheckJudgeModelPresenceResult | undefined;
|
|
97
|
+
issues: string[];
|
|
98
|
+
warnings: string[];
|
|
99
|
+
notes: string[];
|
|
100
|
+
}>;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
4
|
+
import { configPathFor, loadApprovalState, loadConfigFile, repoLocalStateDirFor, resolveAdapterName, writeConfigFile, } from '../config-io.js';
|
|
5
|
+
import { appendCliAuditEvent } from '../core/audit-io.js';
|
|
6
|
+
import { JUDGE_CLOUD_CONSENT_REASON } from '../core/capability/reasons.js';
|
|
7
|
+
import { belayStateDir, normalizeJudgeConfig } from '../core/config.js';
|
|
8
|
+
import { writeJudgeCredentialStore } from '../core/credential-store.js';
|
|
9
|
+
import { refreshIntegrityIfPinned } from '../core/integrity.js';
|
|
10
|
+
import { resolveJudgeCredential } from '../core/judge-api-key.js';
|
|
11
|
+
import { ensurePendingJudgeCloudConsentApproval } from '../core/judge-cloud-consent.js';
|
|
12
|
+
import { hasValidCloudConsent, isCloudJudgeConfig, resolveJudgeUsePatch, } from '../core/judge-config.js';
|
|
13
|
+
import { diagnoseJudge } from '../core/judge-doctor.js';
|
|
14
|
+
import { rejectDeprecatedJudgeModelAuto } from '../core/judge-model-policy.js';
|
|
15
|
+
import { resolveJudgeTransport } from '../core/judge-runtime-detection.js';
|
|
16
|
+
import { getJudgeProviderSpec, isJudgeProviderId, JUDGE_CATALOG, JUDGE_PROVIDER_IDS, normalizeLegacyProviderId, } from '../core/verdict/judge-catalog.js';
|
|
17
|
+
import { resolveJudgeModel } from '../core/verdict/judge-factory.js';
|
|
18
|
+
import { readKeyFromStdin } from './stdin-key.js';
|
|
19
|
+
export function isInteractiveTTY() {
|
|
20
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
21
|
+
}
|
|
22
|
+
async function confirmCloudConsent(endpoint) {
|
|
23
|
+
if (!isInteractiveTTY()) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const rl = createInterface({ input, output });
|
|
27
|
+
try {
|
|
28
|
+
const answer = await rl.question(`Cloud judge sends redacted commands to ${endpoint}. Continue? [y/N] `);
|
|
29
|
+
return ['y', 'yes'].includes(answer.trim().toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
rl.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function hasApprovedCloudConsent(repoRoot, config, approvalId) {
|
|
36
|
+
const approved = await loadApprovalState(repoRoot, 'approved-approvals.json', config);
|
|
37
|
+
return approved.approvals.some((entry) => entry.approvalId === approvalId && entry.reason === 'judge_cloud_consent');
|
|
38
|
+
}
|
|
39
|
+
function formatJudgeDiff(before, after) {
|
|
40
|
+
const lines = ['Judge config changes:'];
|
|
41
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
42
|
+
for (const key of keys) {
|
|
43
|
+
const left = JSON.stringify(before[key]);
|
|
44
|
+
const right = JSON.stringify(after[key]);
|
|
45
|
+
if (left !== right) {
|
|
46
|
+
lines.push(` ${String(key)}: ${left} -> ${right}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
}
|
|
51
|
+
export async function judgeStatus(options = {}) {
|
|
52
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
53
|
+
const config = await loadConfigFile(repoRoot);
|
|
54
|
+
const judge = config.judge;
|
|
55
|
+
const spec = judge.providerId ? getJudgeProviderSpec(judge.providerId) : null;
|
|
56
|
+
const repoLocalDir = repoLocalStateDirFor(repoRoot, config);
|
|
57
|
+
const credential = await resolveJudgeCredential({
|
|
58
|
+
judge,
|
|
59
|
+
catalogSpec: spec ?? undefined,
|
|
60
|
+
repoRoot,
|
|
61
|
+
repoLocalStateDir: repoLocalDir,
|
|
62
|
+
config,
|
|
63
|
+
});
|
|
64
|
+
const { resolved } = resolveJudgeModel(judge);
|
|
65
|
+
const transport = resolveJudgeTransport(judge);
|
|
66
|
+
const lines = [
|
|
67
|
+
`Judge providerId : ${judge.providerId ?? '(inferred)'}`,
|
|
68
|
+
`Judge driver : ${judge.provider}`,
|
|
69
|
+
`Transport : ${transport}`,
|
|
70
|
+
`Endpoint : ${judge.endpoint ?? '(none)'}`,
|
|
71
|
+
`Model : ${resolved} (requested: ${judge.model})`,
|
|
72
|
+
`Credential : ${credential.mode} (${credential.sourceKind})${credential.source ? ` → ${credential.source}` : ''} ${credential.key ? '✓ set' : '✗ missing'}`,
|
|
73
|
+
`Cloud consent : ${judge.cloudConsent?.accepted
|
|
74
|
+
? `accepted ${judge.cloudConsent.at} by ${judge.cloudConsent.by}`
|
|
75
|
+
: 'not recorded'}`,
|
|
76
|
+
`Tier1 fallback : ask (fail-closed) — judge unreachable would block, not allow`,
|
|
77
|
+
];
|
|
78
|
+
if (options.json) {
|
|
79
|
+
return {
|
|
80
|
+
providerId: judge.providerId,
|
|
81
|
+
provider: judge.provider,
|
|
82
|
+
endpoint: judge.endpoint,
|
|
83
|
+
model: judge.model,
|
|
84
|
+
modelResolved: resolved,
|
|
85
|
+
transport,
|
|
86
|
+
credential,
|
|
87
|
+
cloudConsent: judge.cloudConsent ?? null,
|
|
88
|
+
cloudJudgeActive: isCloudJudgeConfig(judge) && hasValidCloudConsent(judge),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
export function judgeList(options = {}) {
|
|
94
|
+
const entries = JUDGE_PROVIDER_IDS.map((id) => {
|
|
95
|
+
const spec = JUDGE_CATALOG[id];
|
|
96
|
+
return {
|
|
97
|
+
id,
|
|
98
|
+
driver: spec.driver,
|
|
99
|
+
cloud: spec.isCloud,
|
|
100
|
+
defaultEndpoint: spec.defaultEndpoint,
|
|
101
|
+
defaultModel: spec.defaultModel,
|
|
102
|
+
apiKeyEnvVars: spec.apiKeyEnvVars,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
if (options.json) {
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
return entries
|
|
109
|
+
.map((entry) => `${entry.id} (${entry.cloud ? 'cloud' : 'local'}) driver=${entry.driver} model=${entry.defaultModel} endpoint=${entry.defaultEndpoint ?? 'optional'}`)
|
|
110
|
+
.join('\n');
|
|
111
|
+
}
|
|
112
|
+
export async function judgeUse(options) {
|
|
113
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
114
|
+
const config = await loadConfigFile(repoRoot);
|
|
115
|
+
const adapter = resolveAdapterName(config);
|
|
116
|
+
const providerId = options.providerId?.trim();
|
|
117
|
+
if (!providerId || !isJudgeProviderId(providerId)) {
|
|
118
|
+
throw new Error(`judge use requires provider-id: ${JUDGE_PROVIDER_IDS.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
const before = { ...config.judge };
|
|
121
|
+
if (options.model !== undefined) {
|
|
122
|
+
rejectDeprecatedJudgeModelAuto(options.model);
|
|
123
|
+
}
|
|
124
|
+
if (options.keyStdin &&
|
|
125
|
+
options.credentialMode === 'apiKey' &&
|
|
126
|
+
options.acceptCloud &&
|
|
127
|
+
isInteractiveTTY()) {
|
|
128
|
+
throw new Error('Cannot combine --accept-cloud with --key-stdin in interactive mode. Use env for the API key, or record consent separately.');
|
|
129
|
+
}
|
|
130
|
+
let keyFromStdin;
|
|
131
|
+
if (options.keyStdin && options.credentialMode === 'apiKey') {
|
|
132
|
+
keyFromStdin = await readKeyFromStdin();
|
|
133
|
+
if (!keyFromStdin) {
|
|
134
|
+
throw new Error('--key-stdin requires a non-empty API key on stdin.');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
let interactiveConsentApproved = false;
|
|
138
|
+
if (options.acceptCloud && isInteractiveTTY()) {
|
|
139
|
+
const spec = getJudgeProviderSpec(providerId);
|
|
140
|
+
const endpoint = options.endpoint?.trim() || spec?.defaultEndpoint || before.endpoint?.trim() || '';
|
|
141
|
+
if (endpoint) {
|
|
142
|
+
interactiveConsentApproved = await confirmCloudConsent(endpoint);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const cloudConsentApprovalId = options.cloudConsentApprovalId;
|
|
146
|
+
if (options.cloudConsentApprovalId) {
|
|
147
|
+
const ok = await hasApprovedCloudConsent(repoRoot, config, options.cloudConsentApprovalId);
|
|
148
|
+
if (!ok) {
|
|
149
|
+
throw new Error(`Approval ${options.cloudConsentApprovalId} is not an approved judge_cloud_consent.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (options.acceptCloud &&
|
|
153
|
+
!isInteractiveTTY() &&
|
|
154
|
+
getJudgeProviderSpec(providerId)?.isCloud) {
|
|
155
|
+
throw new Error('--accept-cloud has no effect in non-interactive mode. Use TTY confirmation or --cloud-consent-approval-id.');
|
|
156
|
+
}
|
|
157
|
+
const patch = resolveJudgeUsePatch(before, {
|
|
158
|
+
providerId,
|
|
159
|
+
model: options.model,
|
|
160
|
+
endpoint: options.endpoint,
|
|
161
|
+
timeoutMs: options.timeoutMs,
|
|
162
|
+
acceptCloud: options.acceptCloud,
|
|
163
|
+
cloudConsentApprovalId,
|
|
164
|
+
credentialMode: options.credentialMode,
|
|
165
|
+
keyEnv: options.keyEnv,
|
|
166
|
+
interactiveTTY: isInteractiveTTY(),
|
|
167
|
+
interactiveConsentApproved,
|
|
168
|
+
});
|
|
169
|
+
if (patch.errors.length > 0) {
|
|
170
|
+
throw new Error(patch.errors.join(' '));
|
|
171
|
+
}
|
|
172
|
+
if (keyFromStdin !== undefined) {
|
|
173
|
+
const stateDir = belayStateDir(config, repoLocalStateDirFor(repoRoot, config));
|
|
174
|
+
await writeJudgeCredentialStore(stateDir, keyFromStdin);
|
|
175
|
+
}
|
|
176
|
+
const after = normalizeJudgeConfig(patch.judge);
|
|
177
|
+
const updated = { ...config, judge: after };
|
|
178
|
+
await writeConfigFile(repoRoot, updated, adapter);
|
|
179
|
+
await refreshIntegrityIfPinned(repoRoot, updated);
|
|
180
|
+
await appendCliAuditEvent(repoRoot, updated, {
|
|
181
|
+
event: 'judge_provider_changed',
|
|
182
|
+
from: { providerId: before.providerId, provider: before.provider },
|
|
183
|
+
to: { providerId: after.providerId, provider: after.provider },
|
|
184
|
+
by: 'belay judge use',
|
|
185
|
+
});
|
|
186
|
+
if (after.cloudConsent?.accepted && !before.cloudConsent?.accepted) {
|
|
187
|
+
await appendCliAuditEvent(repoRoot, updated, {
|
|
188
|
+
event: 'judge_cloud_consent_recorded',
|
|
189
|
+
providerId: after.cloudConsent.providerId,
|
|
190
|
+
endpoint: after.cloudConsent.endpoint,
|
|
191
|
+
by: after.cloudConsent.by,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const spec = getJudgeProviderSpec(providerId);
|
|
195
|
+
const credential = await resolveJudgeCredential({
|
|
196
|
+
judge: after,
|
|
197
|
+
catalogSpec: spec ?? undefined,
|
|
198
|
+
repoRoot,
|
|
199
|
+
repoLocalStateDir: repoLocalStateDirFor(repoRoot, updated),
|
|
200
|
+
config: updated,
|
|
201
|
+
});
|
|
202
|
+
if (spec?.isCloud && !credential.key) {
|
|
203
|
+
patch.warnings.push(`API key not found for ${providerId}. Tier1 cloud judge will fail closed to ask.`);
|
|
204
|
+
}
|
|
205
|
+
const result = {
|
|
206
|
+
ok: true,
|
|
207
|
+
configPath: configPathFor(repoRoot, adapter),
|
|
208
|
+
diff: formatJudgeDiff(before, after),
|
|
209
|
+
warnings: patch.warnings,
|
|
210
|
+
judge: after,
|
|
211
|
+
};
|
|
212
|
+
if (options.json) {
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
const parts = [
|
|
216
|
+
result.diff,
|
|
217
|
+
...result.warnings.map((w) => `Warning: ${w}`),
|
|
218
|
+
'Judge config updated.',
|
|
219
|
+
];
|
|
220
|
+
return parts.join('\n');
|
|
221
|
+
}
|
|
222
|
+
export async function judgeRequestCloudConsent(options = {}) {
|
|
223
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
224
|
+
const config = await loadConfigFile(repoRoot);
|
|
225
|
+
const providerId = options.providerId?.trim();
|
|
226
|
+
if (!providerId || !isJudgeProviderId(providerId)) {
|
|
227
|
+
throw new Error(`judge consent requires --provider-id: ${JUDGE_PROVIDER_IDS.join(', ')}`);
|
|
228
|
+
}
|
|
229
|
+
const canonicalProviderId = normalizeLegacyProviderId(providerId);
|
|
230
|
+
const spec = getJudgeProviderSpec(canonicalProviderId);
|
|
231
|
+
if (!spec?.isCloud) {
|
|
232
|
+
throw new Error(`judge consent applies only to cloud providers (${JUDGE_PROVIDER_IDS.filter((id) => JUDGE_CATALOG[id].isCloud).join(', ')}).`);
|
|
233
|
+
}
|
|
234
|
+
const endpoint = options.endpoint?.trim() || config.judge.endpoint?.trim() || spec.defaultEndpoint || '';
|
|
235
|
+
if (!endpoint) {
|
|
236
|
+
throw new Error(`${canonicalProviderId} requires --endpoint for cloud consent request.`);
|
|
237
|
+
}
|
|
238
|
+
const { approvalId, created } = await ensurePendingJudgeCloudConsentApproval({
|
|
239
|
+
repoRoot,
|
|
240
|
+
config,
|
|
241
|
+
providerId: canonicalProviderId,
|
|
242
|
+
endpoint,
|
|
243
|
+
});
|
|
244
|
+
const lines = [
|
|
245
|
+
`Cloud consent approval ${created ? 'created' : 'reused'}: ${approvalId}`,
|
|
246
|
+
`Approve: belay approve ${approvalId}`,
|
|
247
|
+
`Then: belay judge use ${canonicalProviderId} --endpoint ${endpoint} --cloud-consent-approval-id ${approvalId}`,
|
|
248
|
+
];
|
|
249
|
+
if (options.json) {
|
|
250
|
+
return { approvalId, created, providerId, endpoint, reason: JUDGE_CLOUD_CONSENT_REASON };
|
|
251
|
+
}
|
|
252
|
+
return lines.join('\n');
|
|
253
|
+
}
|
|
254
|
+
export async function judgeTest(options = {}) {
|
|
255
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
256
|
+
const config = await loadConfigFile(repoRoot);
|
|
257
|
+
const diagnosis = await diagnoseJudge(config, repoRoot, {
|
|
258
|
+
liveProbe: options.liveProbe === true,
|
|
259
|
+
});
|
|
260
|
+
const modelCheck = diagnosis.modelCheck;
|
|
261
|
+
if (options.json) {
|
|
262
|
+
return { ...diagnosis, modelCheck };
|
|
263
|
+
}
|
|
264
|
+
return [
|
|
265
|
+
`Model check: ${modelCheck?.status ?? 'unverified'}`,
|
|
266
|
+
`Model source: ${modelCheck?.source ?? 'unknown'}`,
|
|
267
|
+
...diagnosis.notes.map((n) => `Note: ${n}`),
|
|
268
|
+
...diagnosis.warnings.map((w) => `Warning: ${w}`),
|
|
269
|
+
...diagnosis.issues.map((i) => `Issue: ${i}`),
|
|
270
|
+
].join('\n');
|
|
271
|
+
}
|
|
272
|
+
export async function judgeBench(options = {}) {
|
|
273
|
+
const { judgeLatencySloReport } = await import('../core/verdict/judge-baseline.js');
|
|
274
|
+
const report = judgeLatencySloReport();
|
|
275
|
+
if (options.json) {
|
|
276
|
+
return report;
|
|
277
|
+
}
|
|
278
|
+
const lines = [
|
|
279
|
+
`SLO target: Tier1 session p95 <= ${Math.round(report.slo.spawnBaselineP95Ms * (1 - report.slo.tier1P95ReductionTarget))}ms`,
|
|
280
|
+
`Tier0 p50/p95: ${report.measured.tier0.p50}/${report.measured.tier0.p95}ms (${report.measured.tier0.count} samples)`,
|
|
281
|
+
`Tier1 spawn p50/p95: ${report.measured.tier1_spawn.p50}/${report.measured.tier1_spawn.p95}ms (${report.measured.tier1_spawn.count} samples)`,
|
|
282
|
+
`Tier1 session p50/p95: ${report.measured.tier1_session.p50}/${report.measured.tier1_session.p95}ms (${report.measured.tier1_session.count} samples)`,
|
|
283
|
+
`Session meets target: ${report.sessionMeetsTarget ? 'yes' : 'no'}`,
|
|
284
|
+
];
|
|
285
|
+
return lines.join('\n');
|
|
286
|
+
}
|
|
287
|
+
export async function runJudgeCommand(options) {
|
|
288
|
+
switch (options.subcommand) {
|
|
289
|
+
case 'status':
|
|
290
|
+
return judgeStatus(options);
|
|
291
|
+
case 'list':
|
|
292
|
+
return judgeList(options);
|
|
293
|
+
case 'use':
|
|
294
|
+
return judgeUse(options);
|
|
295
|
+
case 'test':
|
|
296
|
+
return judgeTest(options);
|
|
297
|
+
case 'bench':
|
|
298
|
+
return judgeBench(options);
|
|
299
|
+
case 'consent':
|
|
300
|
+
return judgeRequestCloudConsent(options);
|
|
301
|
+
default:
|
|
302
|
+
throw new Error('judge requires subcommand: status, list, use, test, bench, or consent');
|
|
303
|
+
}
|
|
304
|
+
}
|
package/dist/commands/metrics.js
CHANGED
|
@@ -2,6 +2,12 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loadConfigFile } from '../config-io.js';
|
|
4
4
|
import { computeAuditMetrics, parseAuditNdjson } from '../core/audit-metrics.js';
|
|
5
|
+
function formatFingerprintPreview(fingerprint) {
|
|
6
|
+
if (fingerprint.length <= 12) {
|
|
7
|
+
return fingerprint;
|
|
8
|
+
}
|
|
9
|
+
return `${fingerprint.slice(0, 12)}…`;
|
|
10
|
+
}
|
|
5
11
|
export async function metricsProject(options = {}) {
|
|
6
12
|
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
7
13
|
const config = await loadConfigFile(repoRoot);
|
|
@@ -26,6 +32,7 @@ export function formatMetricsReport(report) {
|
|
|
26
32
|
`Schema: v${report.schemaVersion}`,
|
|
27
33
|
`Gate events: ${report.gateEvents}`,
|
|
28
34
|
`Would-block: ${report.wouldBlockCount} (${(report.wouldBlockRate * 100).toFixed(1)}%)`,
|
|
35
|
+
`Classifier-quality would-block: ${report.classifierWouldBlockCount} (${(report.classifierWouldBlockRate * 100).toFixed(1)}%)`,
|
|
29
36
|
`Approvals recorded during audit: ${report.approvalRecordedCount}`,
|
|
30
37
|
];
|
|
31
38
|
if (report.approvalLatency.count > 0) {
|
|
@@ -34,6 +41,31 @@ export function formatMetricsReport(report) {
|
|
|
34
41
|
if (report.bypassAttemptCount > 0) {
|
|
35
42
|
lines.push(`Bypass attempts detected: ${report.bypassAttemptCount}`);
|
|
36
43
|
}
|
|
44
|
+
if (report.availabilityAsks.total > 0) {
|
|
45
|
+
lines.push('', 'Availability-caused asks (infrastructure, not classifier ground truth):');
|
|
46
|
+
lines.push(`- missing trusted cwd: ${report.availabilityAsks.missingTrustedCwd}`);
|
|
47
|
+
lines.push(`- judge timeout: ${report.availabilityAsks.judgeTimeout}`);
|
|
48
|
+
lines.push(`- other judge fallback: ${report.availabilityAsks.judgeFallback}`);
|
|
49
|
+
lines.push(`- total: ${report.availabilityAsks.total}`);
|
|
50
|
+
}
|
|
51
|
+
if (report.repeatedFingerprintAsks.length > 0) {
|
|
52
|
+
lines.push('', 'Repeated fingerprint asks (repeat friction):');
|
|
53
|
+
for (const entry of report.repeatedFingerprintAsks) {
|
|
54
|
+
lines.push(`- [${entry.reason}] x${entry.askCount} ${formatFingerprintPreview(entry.fingerprint)}: ${entry.summary}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (Object.keys(report.wouldBlockByReason).length > 0) {
|
|
58
|
+
lines.push('', 'Would-block by reason:');
|
|
59
|
+
for (const [reason, count] of Object.entries(report.wouldBlockByReason).sort((a, b) => b[1] - a[1])) {
|
|
60
|
+
lines.push(`- ${reason}: ${count}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (report.approvalRatioByReason.length > 0) {
|
|
64
|
+
lines.push('', 'Approval ratio by reason (candidate signal, not ground truth):');
|
|
65
|
+
for (const entry of report.approvalRatioByReason.slice(0, 10)) {
|
|
66
|
+
lines.push(`- ${entry.reason}: ${(entry.approvalRate * 100).toFixed(0)}% approved after deny (${entry.approvedAfterDenyCount}/${entry.wouldBlockCount})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
37
69
|
if (Object.keys(report.byVerdict).length > 0) {
|
|
38
70
|
lines.push('', 'By verdict:');
|
|
39
71
|
for (const [verdict, count] of Object.entries(report.byVerdict).sort((a, b) => b[1] - a[1])) {
|
|
@@ -61,13 +93,13 @@ export function formatMetricsReport(report) {
|
|
|
61
93
|
}
|
|
62
94
|
}
|
|
63
95
|
if (report.noisyRuleCandidates.length > 0) {
|
|
64
|
-
lines.push('', 'Noisy rule candidates:');
|
|
96
|
+
lines.push('', 'Noisy rule candidates (high-signal subset of approval ratios; ≥50% approved after deny):');
|
|
65
97
|
for (const rule of report.noisyRuleCandidates) {
|
|
66
98
|
lines.push(`- ${rule.reason}: ${(rule.approvalRate * 100).toFixed(0)}% approved after deny (${rule.approvedCount}/${rule.denyCount})`);
|
|
67
99
|
}
|
|
68
100
|
}
|
|
69
101
|
if (Object.keys(report.byReason).length > 0) {
|
|
70
|
-
lines.push('', '
|
|
102
|
+
lines.push('', 'All gate events by reason:');
|
|
71
103
|
for (const [reason, count] of Object.entries(report.byReason).sort((a, b) => b[1] - a[1])) {
|
|
72
104
|
lines.push(`- ${reason}: ${count}`);
|
|
73
105
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CorpusCategory, CorpusProvenanceCounts } from '../corpus/types.js';
|
|
2
|
+
export declare const QUALITY_REPORT_SCHEMA_VERSION = 1;
|
|
3
|
+
export interface QualityReport {
|
|
4
|
+
schemaVersion: typeof QUALITY_REPORT_SCHEMA_VERSION;
|
|
5
|
+
ok: boolean;
|
|
6
|
+
corpus: {
|
|
7
|
+
path: string;
|
|
8
|
+
passesHardGates: boolean;
|
|
9
|
+
totalCases: number;
|
|
10
|
+
categoryCounts: Record<CorpusCategory, number>;
|
|
11
|
+
provenanceCounts: CorpusProvenanceCounts;
|
|
12
|
+
mustAskMisses: number;
|
|
13
|
+
provablyBenignBlocks: number;
|
|
14
|
+
acceptedBenignMismatches: number;
|
|
15
|
+
accuracy: number;
|
|
16
|
+
};
|
|
17
|
+
audit: {
|
|
18
|
+
logPath: string;
|
|
19
|
+
gateEvents: number;
|
|
20
|
+
classifierWouldBlockRate: number;
|
|
21
|
+
availabilityAsks: number;
|
|
22
|
+
readyForEnforce: boolean;
|
|
23
|
+
repeatedFingerprintPatterns: number;
|
|
24
|
+
};
|
|
25
|
+
harvest: {
|
|
26
|
+
scope: 'shell';
|
|
27
|
+
benignCandidates: number;
|
|
28
|
+
availabilityQueue: number;
|
|
29
|
+
};
|
|
30
|
+
notes: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface QualityOptions {
|
|
33
|
+
targetDir?: string;
|
|
34
|
+
corpusDir?: string;
|
|
35
|
+
json?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare function qualityCheck(options?: QualityOptions): Promise<QualityReport>;
|
|
38
|
+
export declare function formatQualityReport(report: QualityReport): string;
|