harness-everything 0.29.0 → 0.30.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +42 -0
- package/PRIVACY.md +9 -0
- package/benchmarks/fixtures/system-one-intent-holdout-draft.json +2228 -0
- package/benchmarks/fixtures/system-one-intent-holdout.json +16509 -0
- package/benchmarks/fixtures/system-one-intent-reviews.json +2723 -0
- package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/README.md +64 -0
- package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/report.json +15177 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-2026-09-24/report.json +2976 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-005-steps.json +6125 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-haiku-sonnet-escalation.json +6344 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-scaled-margin-escalation.json +6206 -0
- package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report.json +6103 -0
- package/benchmarks/results/system-one/laya-ft-2026-09-25/README.md +83 -0
- package/benchmarks/results/system-one/laya-ft-2026-09-25/report.json +71 -0
- package/benchmarks/results/system-one/laya-ft-2026-09-25/shadow-report.json +44 -0
- package/benchmarks/results/system-one/laya-zs-2026-09-25/README.md +68 -0
- package/benchmarks/results/system-one/laya-zs-2026-09-25/report.json +173 -0
- package/ci/fable-mode-test.js +16 -0
- package/ci/mechanism-34-observations.test.js +550 -0
- package/ci/mechanism-system-one-collect.test.js +16 -0
- package/ci/mechanism-system-one-intent.test.js +406 -0
- package/ci/mechanism-system-one-laya.test.js +45 -0
- package/ci/mechanism-system-one-relevance.test.js +112 -0
- package/ci/mechanism-system-one-resident.test.js +16 -0
- package/ci/system-one-laya-calibrate-test.py +65 -0
- package/ci/system-one-laya-export-test.py +155 -0
- package/ci/system-one-laya-label-test.py +201 -0
- package/ci/system-one-laya-perintent-test.py +60 -0
- package/ci/system-one-laya-provider-test.py +115 -0
- package/ci/system-one-laya-train-test.py +93 -0
- package/docs/routing.md +25 -0
- package/docs/system-one-intent-gates.md +102 -0
- package/docs/system-one-intent.md +173 -0
- package/docs/system-one-laya-training.md +38 -0
- package/docs/system-one-observations.md +206 -0
- package/docs/system-one-routing.md +25 -10
- package/docs/system-one-suggestion-gates.md +292 -0
- package/docs/system-one-training.md +56 -6
- package/docs/workflows/fable-mode.md +7 -0
- package/fable-mode/SKILL.md +1 -1
- package/fable-mode/agents/fable-orchestrator.md +4 -4
- package/fable-mode/agents/fable-worker-haiku.md +4 -4
- package/fable-mode/agents/fable-worker-sonnet.md +4 -4
- package/fable-mode/execution-guardrails/SKILL.md +5 -3
- package/fable-mode/execution-guardrails/references/guardrail-rules.md +12 -8
- package/fable-mode/fable-haiku/SKILL.md +1 -1
- package/fable-mode/fable-opus/SKILL.md +1 -1
- package/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/kernel-router.js +10 -0
- package/harness-everything/scripts/system-one/catalogs.js +35 -0
- package/harness-everything/scripts/system-one/contract.js +45 -1
- package/harness-everything/scripts/system-one/evaluate.js +115 -19
- package/harness-everything/scripts/system-one/intent.js +22 -0
- package/harness-everything/scripts/system-one/laya_adapter.py +141 -0
- package/harness-everything/scripts/system-one/provider.js +10 -3
- package/harness-everything/scripts/system-one/resident.js +8 -3
- package/harness-everything/scripts/system-one/router.js +1 -1
- package/hooks/hooks.json +40 -0
- package/hooks/scripts/codex-user-prompt.js +39 -0
- package/hooks/scripts/lib/observations.js +581 -0
- package/hooks/scripts/observation-hook.js +25 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/hooks.json +35 -2
- package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +39 -0
- package/plugins/harness-everything/hooks/scripts/lib/observations.js +581 -0
- package/plugins/harness-everything/hooks/scripts/observation-hook.js +25 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +4 -4
- package/plugins/harness-everything/skills/fable-mode/agents/fable-worker-haiku.md +4 -4
- package/plugins/harness-everything/skills/fable-mode/agents/fable-worker-sonnet.md +4 -4
- package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +5 -3
- package/plugins/harness-everything/skills/fable-mode/execution-guardrails/references/guardrail-rules.md +12 -8
- package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +10 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/catalogs.js +35 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/contract.js +45 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/evaluate.js +115 -19
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/intent.js +22 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/laya_adapter.py +141 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/provider.js +10 -3
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/resident.js +8 -3
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/router.js +1 -1
- package/sbom.cdx.json +7 -7
- package/scripts/evaluate-system-one.js +17 -5
- package/scripts/system-one-calibrate-laya.py +165 -0
- package/scripts/system-one-calibrate.js +42 -13
- package/scripts/system-one-collect.js +7 -3
- package/scripts/system-one-compare-laya.py +156 -0
- package/scripts/system-one-corpus.js +22 -15
- package/scripts/system-one-eval-laya.js +112 -0
- package/scripts/system-one-export-laya.py +197 -0
- package/scripts/system-one-label-laya.py +243 -0
- package/scripts/system-one-label.js +163 -37
- package/scripts/system-one-observations-export.js +106 -0
- package/scripts/system-one-observations-review.js +86 -0
- package/scripts/system-one-perintent-laya.py +134 -0
- package/scripts/system-one-train-laya.py +344 -0
- package/scripts/system-one-train-ngram.py +66 -14
|
@@ -10,7 +10,12 @@ const { spawn } = require('node:child_process');
|
|
|
10
10
|
const { Worker } = require('node:worker_threads');
|
|
11
11
|
const { validateRequest } = require('./contract');
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const ADAPTER_PATTERN = /^[a-z][a-z0-9_-]{0,63}\.py$/;
|
|
14
|
+
const adapterPath = manifest => {
|
|
15
|
+
const name = manifest.adapter || 'cua_adapter.py';
|
|
16
|
+
if (!ADAPTER_PATTERN.test(name)) throw new TypeError('invalid-adapter');
|
|
17
|
+
return path.join(__dirname, name);
|
|
18
|
+
};
|
|
14
19
|
const LOCK_TTL_MS = 60000;
|
|
15
20
|
const CALL_TIMEOUT_MS = 1000;
|
|
16
21
|
const MAX_REPLY_BYTES = 1024 * 1024;
|
|
@@ -18,7 +23,7 @@ const unavailable = reason => ({ status: 'unavailable', reason });
|
|
|
18
23
|
const sleep = ms => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
19
24
|
const exact = (value, keys) => value && typeof value === 'object' && !Array.isArray(value)
|
|
20
25
|
&& Object.keys(value).length === keys.length && keys.every(k => Object.hasOwn(value, k));
|
|
21
|
-
const defaultCommand = manifest => [manifest.python, [
|
|
26
|
+
const defaultCommand = manifest => [manifest.python, [adapterPath(manifest), '--serve']];
|
|
22
27
|
|
|
23
28
|
function stateFiles(manifest, manifestPath) {
|
|
24
29
|
const digest = createHash('sha256').update(JSON.stringify(manifest)).digest('hex').slice(0, 16);
|
|
@@ -197,4 +202,4 @@ if (require.main === module) {
|
|
|
197
202
|
if (op === 'stop') stop(manifest, manifestPath);
|
|
198
203
|
process.stdout.write(`${JSON.stringify(status(manifest, manifestPath))}\n`);
|
|
199
204
|
}
|
|
200
|
-
module.exports = { stateFiles, readState, callSync, callAsync, score, scoreAsync, ensureReady, status, stop, LOCK_TTL_MS, CALL_TIMEOUT_MS };
|
|
205
|
+
module.exports = { stateFiles, readState, callSync, callAsync, score, scoreAsync, ensureReady, status, stop, defaultCommand, LOCK_TTL_MS, CALL_TIMEOUT_MS };
|
|
@@ -26,7 +26,7 @@ function selectTier(input, env = process.env, scorer = provider.score) {
|
|
|
26
26
|
const allowed = ['provider-timeout', 'provider-output-limit', 'provider-exit', 'provider-json', 'provider-config', 'provider-unavailable', 'provider-starting'];
|
|
27
27
|
return fallback(allowed.includes(result?.reason) ? result.reason : 'provider-unavailable');
|
|
28
28
|
}
|
|
29
|
-
const decision = decide(request, result.response,
|
|
29
|
+
const decision = decide(request, result.response, provider.decisionPolicy(result.acceptance));
|
|
30
30
|
if (decision.selectedId === 'unclassified') Object.assign(decision, { selectedId: null, status: 'abstain', reason: 'unclassified' });
|
|
31
31
|
const accepted = decision.status === 'accepted';
|
|
32
32
|
const belowFloor = accepted && input.floor != null && RANK[decision.selectedId] < RANK[input.floor];
|
package/sbom.cdx.json
CHANGED
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
"$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.7",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:4d14e861-0454-56ef-a99b-c25f67dc9e83",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-
|
|
8
|
+
"timestamp": "2026-10-01T02:23:01.933Z",
|
|
9
9
|
"tools": [
|
|
10
10
|
{
|
|
11
11
|
"vendor": "Harness Everything",
|
|
12
12
|
"name": "generate-sbom.js",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.30.1"
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
16
|
"component": {
|
|
17
17
|
"type": "application",
|
|
18
|
-
"bom-ref": "pkg:npm/harness-everything@0.
|
|
18
|
+
"bom-ref": "pkg:npm/harness-everything@0.30.1",
|
|
19
19
|
"name": "harness-everything",
|
|
20
|
-
"version": "0.
|
|
21
|
-
"purl": "pkg:npm/harness-everything@0.
|
|
20
|
+
"version": "0.30.1",
|
|
21
|
+
"purl": "pkg:npm/harness-everything@0.30.1"
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"components": [
|
|
@@ -1999,7 +1999,7 @@
|
|
|
1999
1999
|
]
|
|
2000
2000
|
},
|
|
2001
2001
|
{
|
|
2002
|
-
"ref": "pkg:npm/harness-everything@0.
|
|
2002
|
+
"ref": "pkg:npm/harness-everything@0.30.1",
|
|
2003
2003
|
"dependsOn": [
|
|
2004
2004
|
"pkg:npm/%40commitlint/cli@19.8.1",
|
|
2005
2005
|
"pkg:npm/%40commitlint/config-conventional@19.8.1",
|
|
@@ -4,7 +4,8 @@ const fs = require('node:fs');
|
|
|
4
4
|
const os = require('node:os');
|
|
5
5
|
const { performance } = require('node:perf_hooks');
|
|
6
6
|
const { validateCorpus, evaluate } = require('../harness-everything/scripts/system-one/evaluate');
|
|
7
|
-
const { scoreAsync, provenance, readManifest } = require('../harness-everything/scripts/system-one/provider');
|
|
7
|
+
const { scoreAsync, provenance, readManifest, decisionPolicy } = require('../harness-everything/scripts/system-one/provider');
|
|
8
|
+
const { CATALOGS, MULTI } = require('../harness-everything/scripts/system-one/catalogs');
|
|
8
9
|
const resident = require('../harness-everything/scripts/system-one/resident');
|
|
9
10
|
const { decide } = require('../harness-everything/scripts/system-one/contract');
|
|
10
11
|
const { run: route } = require('../harness-everything/scripts/tier-router');
|
|
@@ -22,6 +23,15 @@ async function run(corpus, manifest) {
|
|
|
22
23
|
const ready = isResident && resident.ensureReady(config, manifest, 60000);
|
|
23
24
|
let artifactVerified = false;
|
|
24
25
|
if (isNgram) { try { require('../harness-everything/scripts/system-one/ngram').loadModel(config); artifactVerified = true; } catch (_) { /* recorded below */ } }
|
|
26
|
+
const task = corpus.cases[0].request.task;
|
|
27
|
+
// The lexical router only knows tiers; other stages compare against the majority gold (ties by catalog order).
|
|
28
|
+
let majority = null;
|
|
29
|
+
if (task !== 'tier') {
|
|
30
|
+
const ids = CATALOGS[task].map(o => o.id);
|
|
31
|
+
const count = id => corpus.cases.filter(c => c.split === 'holdout' && (c.gold === null ? 'unclassified' : c.gold) === id).length;
|
|
32
|
+
const top = ids.reduce((best, id) => (count(id) > count(best) ? id : best), ids[0]);
|
|
33
|
+
majority = top === 'unclassified' ? null : top;
|
|
34
|
+
}
|
|
25
35
|
const records = [];
|
|
26
36
|
const originalMode = process.env.HARNESS_SYSTEM_ONE_MODE;
|
|
27
37
|
const originalLog = console.log;
|
|
@@ -29,17 +39,17 @@ async function run(corpus, manifest) {
|
|
|
29
39
|
process.env.HARNESS_SYSTEM_ONE_MODE = 'off';
|
|
30
40
|
console.log = () => {};
|
|
31
41
|
for (const c of corpus.cases.filter(item => item.split === 'holdout')) {
|
|
32
|
-
const tier = route(c.request.context).workflowPlan.tier;
|
|
42
|
+
const tier = task === 'tier' ? route(c.request.context).workflowPlan.tier : null;
|
|
33
43
|
const runs = [];
|
|
34
44
|
for (let i = 0; i < 2; i++) {
|
|
35
45
|
const start = performance.now();
|
|
36
46
|
const result = await scoreAsync(c.request, manifest);
|
|
37
|
-
const decision = result.status === 'scored' ? decide(c.request, result.response, result.acceptance
|
|
47
|
+
const decision = result.status === 'scored' ? decide(c.request, result.response, decisionPolicy(result.acceptance)) : result;
|
|
38
48
|
if (decision.selectedId === 'unclassified') Object.assign(decision, { status: 'abstain', reason: 'unclassified', selectedId: null });
|
|
39
49
|
const scores = ['accepted', 'abstain'].includes(decision.status) ? result.response.scores.map(s => s.probability) : null;
|
|
40
50
|
runs.push({ decision, scores, latencyMs: performance.now() - start, coldStart: !warm });
|
|
41
51
|
}
|
|
42
|
-
records.push({ id: c.id, baseline: tier === 'unclassified' ? null : tier, runs });
|
|
52
|
+
records.push({ id: c.id, baseline: task === 'tier' ? (tier === 'unclassified' ? null : tier) : majority, runs });
|
|
43
53
|
}
|
|
44
54
|
} finally {
|
|
45
55
|
if (isResident && !wasRunning) resident.stop(config, manifest);
|
|
@@ -55,7 +65,9 @@ async function run(corpus, manifest) {
|
|
|
55
65
|
// The ngram transport has no Python package to probe; its provenance is the hash-verified artifact.
|
|
56
66
|
const source = isNgram ? { status: 'recorded', transport: 'ngram', artifactVerified } : provenance(manifest);
|
|
57
67
|
const kind = isNgram ? 'offline-ngram' : isResident ? 'offline-resident' : 'offline-one-shot';
|
|
58
|
-
|
|
68
|
+
const secondaryThreshold = MULTI.includes(task) ? config?.acceptance?.secondaryThreshold : undefined;
|
|
69
|
+
return { ...evaluate(corpus, records, source, { secondaryThreshold }), evidence: { kind, artifact, source,
|
|
70
|
+
...(MULTI.includes(task) ? { secondaryThreshold } : {}),
|
|
59
71
|
residentReady: isResident ? ready : null,
|
|
60
72
|
environment: { cpu: os.cpus()[0]?.model || 'unknown', os: `${os.platform()} ${os.release()} ${os.arch()}`, node: process.version,
|
|
61
73
|
python: source.status === 'recorded' && source.provenance ? source.provenance.python : null, threads: 1 },
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Calibrate Laya zero-shot scores to teacher relevance (#255 Phase 3 prep).
|
|
3
|
+
|
|
4
|
+
Per-intent isotonic (PAVA, monotone non-decreasing) mapping from Laya
|
|
5
|
+
zero-shot scores to the sonnet teacher's scaled relevance, plus a null-cutoff
|
|
6
|
+
sweep. Honest estimate via K-fold CV with FIXED derive bands; the shipped
|
|
7
|
+
artifact (knots + cutoff) is fit on all overlap rows and clearly labeled
|
|
8
|
+
in-sample. Stdlib only; no model is loaded -- this runs in seconds.
|
|
9
|
+
"""
|
|
10
|
+
import argparse
|
|
11
|
+
import hashlib
|
|
12
|
+
import importlib.util
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
HERE = Path(__file__).resolve()
|
|
18
|
+
INTENTS = ['explain', 'discuss', 'git', 'fix', 'edit', 'feature', 'refactor',
|
|
19
|
+
'review', 'test', 'docs', 'plan', 'investigate']
|
|
20
|
+
|
|
21
|
+
_compare_spec = importlib.util.spec_from_file_location(
|
|
22
|
+
'laya_compare', HERE.parent / 'system-one-compare-laya.py')
|
|
23
|
+
_compare = importlib.util.module_from_spec(_compare_spec)
|
|
24
|
+
_compare_spec.loader.exec_module(_compare)
|
|
25
|
+
graded = _compare.graded
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def isotonic_fit(xs, ys):
|
|
29
|
+
"""PAVA isotonic regression. Returns knots as [[x, y], ...] sorted by x."""
|
|
30
|
+
by_x = {}
|
|
31
|
+
for x, y in zip(xs, ys):
|
|
32
|
+
by_x.setdefault(float(x), []).append(float(y))
|
|
33
|
+
points = sorted((x, sum(v) / len(v)) for x, v in by_x.items())
|
|
34
|
+
blocks = [[x, y, 1] for x, y in points] # x, mean, weight
|
|
35
|
+
i = 0
|
|
36
|
+
while i < len(blocks) - 1:
|
|
37
|
+
if blocks[i][1] <= blocks[i + 1][1]:
|
|
38
|
+
i += 1
|
|
39
|
+
continue
|
|
40
|
+
x0, m0, w0 = blocks[i]
|
|
41
|
+
x1, m1, w1 = blocks[i + 1]
|
|
42
|
+
blocks[i] = [(x0 * w0 + x1 * w1) / (w0 + w1), (m0 * w0 + m1 * w1) / (w0 + w1), w0 + w1]
|
|
43
|
+
del blocks[i + 1]
|
|
44
|
+
if i > 0:
|
|
45
|
+
i -= 1
|
|
46
|
+
return [[b[0], b[1]] for b in blocks]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def isotonic_predict(knots, x):
|
|
50
|
+
"""Piecewise-linear interpolation over knots, clamped outside the range."""
|
|
51
|
+
x = float(x)
|
|
52
|
+
if x <= knots[0][0]:
|
|
53
|
+
return knots[0][1]
|
|
54
|
+
if x >= knots[-1][0]:
|
|
55
|
+
return knots[-1][1]
|
|
56
|
+
for (x0, y0), (x1, y1) in zip(knots, knots[1:]):
|
|
57
|
+
if x0 <= x <= x1:
|
|
58
|
+
t = (x - x0) / (x1 - x0) if x1 > x0 else 0.0
|
|
59
|
+
return y0 + t * (y1 - y0)
|
|
60
|
+
return knots[-1][1]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def apply_mapping(raw_scores, mappings):
|
|
64
|
+
"""Map raw per-intent scores through per-intent knots (identity when absent)."""
|
|
65
|
+
out = {}
|
|
66
|
+
for intent in INTENTS:
|
|
67
|
+
knots = mappings.get(intent)
|
|
68
|
+
out[intent] = isotonic_predict(knots, raw_scores[intent]) if knots else float(raw_scores[intent])
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def derive_calibrated(raw_scores, mappings, null_cutoff):
|
|
73
|
+
"""Bands on calibrated scores: abstain when the max is at/below the cutoff."""
|
|
74
|
+
scores = apply_mapping(raw_scores, mappings)
|
|
75
|
+
top = max(scores[intent] for intent in INTENTS)
|
|
76
|
+
if top <= null_cutoff:
|
|
77
|
+
return {'gold': None, 'secondary': []}
|
|
78
|
+
gold = max(INTENTS, key=lambda i: (scores[i], -INTENTS.index(i)))
|
|
79
|
+
secondary = sorted((i for i in INTENTS if i != gold and scores[i] >= 0.4),
|
|
80
|
+
key=lambda i: (-scores[i], INTENTS.index(i)))
|
|
81
|
+
return {'gold': gold, 'secondary': secondary}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def fold_of(idx, k):
|
|
85
|
+
return idx % k
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main(argv=None):
|
|
89
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
90
|
+
parser.add_argument('--data-dir', required=True)
|
|
91
|
+
parser.add_argument('--teacher', default='labels-intent-scores.jsonl')
|
|
92
|
+
parser.add_argument('--cand', required=True, help='candidate labels file')
|
|
93
|
+
parser.add_argument('--out', required=True, help='calibration artifact json path')
|
|
94
|
+
parser.add_argument('--folds', type=int, default=5)
|
|
95
|
+
args = parser.parse_args(argv)
|
|
96
|
+
data = Path(args.data_dir)
|
|
97
|
+
teacher = {r['id']: r for r in map(json.loads, (data / args.teacher).read_text(encoding='utf-8').splitlines()) if True}
|
|
98
|
+
cand = {r['id']: r for r in map(json.loads, (data / args.cand).read_text(encoding='utf-8').splitlines()) if True}
|
|
99
|
+
ids = sorted(set(teacher) & set(cand))
|
|
100
|
+
n = len(ids)
|
|
101
|
+
|
|
102
|
+
def fit_mappings(train_ids):
|
|
103
|
+
mappings = {}
|
|
104
|
+
for intent in INTENTS:
|
|
105
|
+
xs = [cand[i]['scores'][intent] for i in train_ids]
|
|
106
|
+
ys = [teacher[i]['scores'][intent] for i in train_ids]
|
|
107
|
+
mappings[intent] = isotonic_fit(xs, ys)
|
|
108
|
+
return mappings
|
|
109
|
+
|
|
110
|
+
# Honest CV with fixed bands (null iff calibrated max <= 0.2).
|
|
111
|
+
cv_exact, cv_graded, cv_mae = [], [], []
|
|
112
|
+
for f in range(args.folds):
|
|
113
|
+
held = [i for idx, i in enumerate(ids) if fold_of(idx, args.folds) == f]
|
|
114
|
+
train = [i for idx, i in enumerate(ids) if fold_of(idx, args.folds) != f]
|
|
115
|
+
mappings = fit_mappings(train)
|
|
116
|
+
exact = sum(1 for i in held
|
|
117
|
+
if derive_calibrated(cand[i]['scores'], mappings, 0.2)['gold'] == teacher[i]['gold'])
|
|
118
|
+
grades = [graded(derive_calibrated(cand[i]['scores'], mappings, 0.2)['gold'],
|
|
119
|
+
derive_calibrated(cand[i]['scores'], mappings, 0.2)['secondary'],
|
|
120
|
+
teacher[i]['gold'], teacher[i].get('secondary', [])) for i in held]
|
|
121
|
+
mae = sum(abs(apply_mapping(cand[i]['scores'], mappings)[k] - teacher[i]['scores'][k])
|
|
122
|
+
for i in held for k in INTENTS) / (len(held) * len(INTENTS))
|
|
123
|
+
cv_exact.append(exact / len(held))
|
|
124
|
+
cv_graded.append(sum(grades) / len(grades))
|
|
125
|
+
cv_mae.append(mae)
|
|
126
|
+
|
|
127
|
+
# Full-fit artifact + null-cutoff sweep (in-sample, labeled as such).
|
|
128
|
+
full = fit_mappings(ids)
|
|
129
|
+
sweep = {}
|
|
130
|
+
for step in range(0, 75, 5):
|
|
131
|
+
cutoff = step / 100
|
|
132
|
+
grades = [graded(*_derive_pair(cand[i]['scores'], full, cutoff, teacher[i])) for i in ids]
|
|
133
|
+
sweep[f'{cutoff:.2f}'] = sum(grades) / len(grades)
|
|
134
|
+
best_cutoff = max(sweep, key=lambda c: (sweep[c], -float(c)))
|
|
135
|
+
mae_full = sum(abs(apply_mapping(cand[i]['scores'], full)[k] - teacher[i]['scores'][k])
|
|
136
|
+
for i in ids for k in INTENTS) / (n * len(INTENTS))
|
|
137
|
+
raw_mae = sum(abs(cand[i]['scores'][k] - teacher[i]['scores'][k])
|
|
138
|
+
for i in ids for k in INTENTS) / (n * len(INTENTS))
|
|
139
|
+
|
|
140
|
+
artifact = {'candidate': args.cand,
|
|
141
|
+
'candidateSha256': hashlib.sha256((data / args.cand).read_bytes()).hexdigest(),
|
|
142
|
+
'teacher': args.teacher,
|
|
143
|
+
'teacherSha256': hashlib.sha256((data / args.teacher).read_bytes()).hexdigest(),
|
|
144
|
+
'n': n, 'folds': args.folds,
|
|
145
|
+
'cv': {'exactMean': sum(cv_exact) / len(cv_exact), 'gradedMean': sum(cv_graded) / len(cv_graded),
|
|
146
|
+
'maeMean': sum(cv_mae) / len(cv_mae)},
|
|
147
|
+
'inSample': {'maeRaw': raw_mae, 'maeCalibrated': mae_full,
|
|
148
|
+
'nullCutoffSweep': sweep, 'bestCutoff': best_cutoff},
|
|
149
|
+
'mappings': full}
|
|
150
|
+
Path(args.out).write_text(json.dumps(artifact, indent=2), encoding='utf-8')
|
|
151
|
+
print(json.dumps({'n': n, 'cvExact': round(artifact['cv']['exactMean'], 3),
|
|
152
|
+
'cvGraded': round(artifact['cv']['gradedMean'], 3),
|
|
153
|
+
'cvMAE': round(artifact['cv']['maeMean'], 4),
|
|
154
|
+
'rawMAE': round(raw_mae, 4), 'bestCutoff': best_cutoff,
|
|
155
|
+
'bestSweepGraded': round(sweep[best_cutoff], 3)}))
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _derive_pair(raw_scores, mappings, cutoff, teacher_row):
|
|
160
|
+
d = derive_calibrated(raw_scores, mappings, cutoff)
|
|
161
|
+
return d['gold'], d['secondary'], teacher_row['gold'], teacher_row.get('secondary', [])
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == '__main__':
|
|
165
|
+
sys.exit(main())
|
|
@@ -3,40 +3,69 @@
|
|
|
3
3
|
// Chooses System One acceptance thresholds on validation scores (docs/system-one-training.md).
|
|
4
4
|
// The holdout is never used here.
|
|
5
5
|
const fs = require('node:fs');
|
|
6
|
+
const { CATALOGS } = require('../harness-everything/scripts/system-one/catalogs');
|
|
7
|
+
const { gradedAgreement } = require('../harness-everything/scripts/system-one/intent');
|
|
6
8
|
|
|
7
9
|
const IDS = ['tier1', 'tier2', 'tier3', 'unclassified'];
|
|
8
10
|
// Same acceptance rule as contract.decide: top probability and top-minus-second margin, unclassified abstains.
|
|
9
|
-
function decideRow(probs, { minConfidence, minMargin }) {
|
|
11
|
+
function decideRow(probs, { minConfidence, minMargin }, ids = IDS) {
|
|
10
12
|
const order = probs.map((p, i) => [p, i]).sort((a, b) => b[0] - a[0] || a[1] - b[1]);
|
|
11
13
|
const [top, second] = order;
|
|
12
14
|
const margin = top[0] - second[0];
|
|
13
|
-
if (margin === 0 || top[0] < minConfidence || margin < minMargin ||
|
|
14
|
-
return
|
|
15
|
+
if (margin === 0 || top[0] < minConfidence || margin < minMargin || ids[top[1]] === 'unclassified') return null;
|
|
16
|
+
return ids[top[1]];
|
|
15
17
|
}
|
|
16
|
-
|
|
18
|
+
// Predicted secondary intents: every other catalog intent at or above the threshold.
|
|
19
|
+
const secondaryOf = (probs, primary, threshold, ids) => ids.filter((id, i) => id !== primary && id !== 'unclassified' && probs[i] >= threshold);
|
|
20
|
+
// Threshold with the best secondary micro-F1 on validation; ties keep the lowest.
|
|
21
|
+
function secondaryThreshold(rows, ids) {
|
|
22
|
+
let best = null;
|
|
23
|
+
for (let s = 5; s <= 50; s += 5) {
|
|
24
|
+
let tp = 0; let fp = 0; let fn = 0;
|
|
25
|
+
for (const r of rows) {
|
|
26
|
+
const primary = ids[r.probs.indexOf(Math.max(...r.probs))];
|
|
27
|
+
const predicted = new Set(secondaryOf(r.probs, primary, s / 100, ids)); const gold = new Set(r.secondary || []);
|
|
28
|
+
for (const x of predicted) (gold.has(x) ? tp++ : fp++);
|
|
29
|
+
for (const x of gold) if (!predicted.has(x)) fn++;
|
|
30
|
+
}
|
|
31
|
+
const f1 = 2 * tp + fp + fn ? 2 * tp / (2 * tp + fp + fn) : 0;
|
|
32
|
+
if (!best || f1 > best.f1) best = { threshold: s / 100, f1 };
|
|
33
|
+
}
|
|
34
|
+
return best.threshold;
|
|
35
|
+
}
|
|
36
|
+
function calibrate(rows, { minPrecision = 0.85, task = 'tier' } = {}) {
|
|
37
|
+
const ids = CATALOGS[task].map(o => o.id);
|
|
38
|
+
const multi = task === 'intent';
|
|
39
|
+
const sThreshold = multi ? secondaryThreshold(rows, ids) : undefined;
|
|
40
|
+
// Intent rows score by graded agreement against { gold, secondary }; tier rows by exact match.
|
|
41
|
+
const score = (r, d) => (multi ? gradedAgreement({ gold: r.gold, secondary: r.secondary || [] }, { gold: d, secondary: secondaryOf(r.probs, d, sThreshold, ids) })
|
|
42
|
+
: Number(d === (r.gold === null ? 'unclassified' : r.gold)));
|
|
17
43
|
let best = null;
|
|
18
44
|
for (let c = 50; c <= 99; c++) {
|
|
19
45
|
for (let m = 0; m <= 90; m += 5) {
|
|
20
46
|
const t = { minConfidence: c / 100, minMargin: m / 100 };
|
|
21
|
-
let accepted = 0; let
|
|
22
|
-
for (const r of rows) { const d = decideRow(r.probs, t); if (d) { accepted++;
|
|
23
|
-
const precision = accepted ?
|
|
47
|
+
let accepted = 0; let points = 0; let exact = 0;
|
|
48
|
+
for (const r of rows) { const d = decideRow(r.probs, t, ids); if (d) { accepted++; points += score(r, d); if (d === r.gold) exact++; } }
|
|
49
|
+
const precision = accepted ? points / accepted : 0;
|
|
24
50
|
if (!accepted || precision < minPrecision) continue;
|
|
25
51
|
const coverage = accepted / rows.length;
|
|
26
52
|
// Highest coverage wins; ties keep the first (lowest) thresholds scanned.
|
|
27
|
-
if (!best || coverage > best.coverage) best = { ...t, coverage, precision, accepted };
|
|
53
|
+
if (!best || coverage > best.coverage) best = { ...t, coverage, precision, accepted, ...(multi ? { exactPrecision: exact / accepted } : {}) };
|
|
28
54
|
}
|
|
29
55
|
}
|
|
30
|
-
|
|
31
|
-
|
|
56
|
+
const extra = multi ? { task, secondaryThreshold: sThreshold } : {};
|
|
57
|
+
return best ? { feasible: true, rows: rows.length, minPrecision, ...extra, ...best }
|
|
58
|
+
: { feasible: false, rows: rows.length, minPrecision, ...extra, minConfidence: null, minMargin: null, coverage: 0, precision: null, accepted: 0 };
|
|
32
59
|
}
|
|
33
60
|
|
|
34
61
|
if (require.main === module) {
|
|
35
62
|
try {
|
|
36
|
-
const
|
|
37
|
-
|
|
63
|
+
const args = process.argv.slice(2);
|
|
64
|
+
const ti = args.indexOf('--task'); const task = ti >= 0 ? args.splice(ti, 2)[1] : 'tier';
|
|
65
|
+
const [input, output, extra] = args;
|
|
66
|
+
if (!input || !output || extra || !Object.hasOwn(CATALOGS, task)) throw new Error('usage: system-one-calibrate.js <val-scores.jsonl> <calibration.json> [--task tier|intent]');
|
|
38
67
|
const rows = fs.readFileSync(input, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
39
|
-
const result = calibrate(rows);
|
|
68
|
+
const result = calibrate(rows, { task });
|
|
40
69
|
fs.writeFileSync(output, `${JSON.stringify(result, null, 2)}\n`);
|
|
41
70
|
console.log(JSON.stringify(result));
|
|
42
71
|
} catch (err) { console.error(`system-one-calibrate: ${err.message}`); process.exitCode = 1; }
|
|
@@ -176,6 +176,9 @@ function build({ home, appdata, holdoutTexts }) {
|
|
|
176
176
|
families: new Set(rows.map(r => r.family)).size, zhShare: rows.length ? +(rows.filter(r => /[一-鿿]/.test(r.text)).length / rows.length).toFixed(3) : 0 } };
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// Every committed holdout; a training prompt must not near-duplicate any of them.
|
|
180
|
+
const defaultHoldouts = () => ['system-one-holdout.json', 'system-one-intent-holdout.json'].map(f => path.join(REPO, 'benchmarks', 'fixtures', f));
|
|
181
|
+
|
|
179
182
|
if (require.main === module) {
|
|
180
183
|
const args = process.argv.slice(2);
|
|
181
184
|
const opt = name => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
|
|
@@ -183,8 +186,9 @@ if (require.main === module) {
|
|
|
183
186
|
const out = path.resolve(opt('--out') || path.join(os.homedir(), '.agents', 'harness-everything', 'system-one', 'training'));
|
|
184
187
|
const rel = path.relative(REPO, out);
|
|
185
188
|
if (!rel.startsWith('..') && !path.isAbsolute(rel)) throw new Error('refusing to write the dataset inside the repository');
|
|
186
|
-
const
|
|
187
|
-
const holdoutTexts =
|
|
189
|
+
const given = args.flatMap((a, i) => (a === '--holdout' && args[i + 1] ? [args[i + 1]] : []));
|
|
190
|
+
const holdoutTexts = (given.length ? given : defaultHoldouts())
|
|
191
|
+
.flatMap(file => JSON.parse(fs.readFileSync(file, 'utf8')).cases.map(c => c.request.context));
|
|
188
192
|
const { rows, stats } = build({ home: opt('--home') || os.homedir(), appdata: opt('--appdata') || process.env.APPDATA || '', holdoutTexts });
|
|
189
193
|
fs.mkdirSync(out, { recursive: true });
|
|
190
194
|
const body = rows.map(r => JSON.stringify(r)).join('\n') + '\n';
|
|
@@ -197,4 +201,4 @@ if (require.main === module) {
|
|
|
197
201
|
process.exitCode = 1;
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
|
-
module.exports = { readSources, keep, redact, dedupe, leakGuard, jaccard, splitFor, build };
|
|
204
|
+
module.exports = { readSources, keep, redact, dedupe, leakGuard, jaccard, splitFor, build, defaultHoldouts };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Compare Laya zero-shot dense labels against the teacher scores (#255 Phase 2 evidence).
|
|
3
|
+
|
|
4
|
+
Reads the private training dir only; the printed/written report holds aggregate
|
|
5
|
+
metrics, hashes and score distributions -- never prompt text -- so it can join
|
|
6
|
+
the public evidence bundle. Graded agreement mirrors
|
|
7
|
+
harness-everything/scripts/system-one/intent.js gradedAgreement; --self-check
|
|
8
|
+
asserts the mirror against the owner's S1-I05 grade table on every run.
|
|
9
|
+
"""
|
|
10
|
+
import argparse
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
INTENTS = ['explain', 'discuss', 'git', 'fix', 'edit', 'feature', 'refactor',
|
|
17
|
+
'review', 'test', 'docs', 'plan', 'investigate']
|
|
18
|
+
|
|
19
|
+
# (gold_a, sec_a, gold_b, sec_b, want) from ci/mechanism-system-one-intent.test.js S1-I05.
|
|
20
|
+
GRADE_TABLE = [
|
|
21
|
+
(('fix', ['test']), ('fix', ['docs', 'review']), 1),
|
|
22
|
+
(('fix', ['test']), ('test', ['fix']), 0.6),
|
|
23
|
+
(('fix', ['test', 'docs', 'review', 'plan']), ('test', ['fix']), 0.3),
|
|
24
|
+
(('fix', ['test', 'docs', 'review']), ('test', ['fix']), 0.6),
|
|
25
|
+
(('fix', ['test']), ('test', []), 0.3),
|
|
26
|
+
(('fix', []), ('docs', ['review']), 0),
|
|
27
|
+
((None, []), (None, []), 1),
|
|
28
|
+
((None, []), ('fix', []), 0),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def graded(a_gold, a_sec, b_gold, b_sec):
|
|
33
|
+
"""Mirror of intent.js gradedAgreement."""
|
|
34
|
+
if a_gold is None or b_gold is None:
|
|
35
|
+
return 1 if (a_gold is None and b_gold is None) else 0
|
|
36
|
+
fa = {a_gold, *a_sec}
|
|
37
|
+
fb = {b_gold, *b_sec}
|
|
38
|
+
if a_gold == b_gold:
|
|
39
|
+
return 1
|
|
40
|
+
if a_gold in fb and b_gold in fa:
|
|
41
|
+
return 0.6 if abs(len(a_sec) - len(b_sec)) < 3 else 0.3
|
|
42
|
+
if a_gold in fb or b_gold in fa:
|
|
43
|
+
return 0.3
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def self_check():
|
|
48
|
+
for (ag, ase), (bg, bse), want in GRADE_TABLE:
|
|
49
|
+
got = graded(ag, ase, bg, bse)
|
|
50
|
+
assert got == want, f'graded mirror drift: {(ag, ase, bg, bse)} -> {got}, want {want}'
|
|
51
|
+
assert graded(bg, bse, ag, ase) == want, 'graded mirror must be symmetric'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_zh(text):
|
|
55
|
+
return any('\u4e00' <= ch <= '\u9fff' for ch in text)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def spearman(xs, ys):
|
|
59
|
+
n = len(xs)
|
|
60
|
+
if n < 2:
|
|
61
|
+
return None
|
|
62
|
+
rx = sorted(range(n), key=lambda i: xs[i])
|
|
63
|
+
ry = sorted(range(n), key=lambda i: ys[i])
|
|
64
|
+
rank_x = [0] * n
|
|
65
|
+
rank_y = [0] * n
|
|
66
|
+
for r, i in enumerate(rx):
|
|
67
|
+
rank_x[i] = r
|
|
68
|
+
for r, i in enumerate(ry):
|
|
69
|
+
rank_y[i] = r
|
|
70
|
+
mean = (n - 1) / 2
|
|
71
|
+
cov = sum((rank_x[i] - mean) * (rank_y[i] - mean) for i in range(n))
|
|
72
|
+
var = sum((rank_x[i] - mean) ** 2 for i in range(n))
|
|
73
|
+
return cov / var if var else None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_jsonl(path):
|
|
77
|
+
return [json.loads(line) for line in Path(path).read_text(encoding='utf-8').splitlines() if line.strip()]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main(argv=None):
|
|
81
|
+
self_check()
|
|
82
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
83
|
+
parser.add_argument('--data-dir', required=True)
|
|
84
|
+
parser.add_argument('--teacher', default='labels-intent-scores.jsonl')
|
|
85
|
+
parser.add_argument('--cand', nargs='+', required=True, help='candidate labels files to compare')
|
|
86
|
+
parser.add_argument('--out', required=True, help='report json path')
|
|
87
|
+
args = parser.parse_args(argv)
|
|
88
|
+
data = Path(args.data_dir)
|
|
89
|
+
prompts = {r['id']: r['text'] for r in load_jsonl(data / 'prompts.jsonl')}
|
|
90
|
+
teacher = {r['id']: r for r in load_jsonl(data / args.teacher)}
|
|
91
|
+
report = {'teacher': args.teacher,
|
|
92
|
+
'teacherSha256': hashlib.sha256((data / args.teacher).read_bytes()).hexdigest(),
|
|
93
|
+
'candidates': {}}
|
|
94
|
+
cands = {name: {r['id']: r for r in load_jsonl(data / name)} for name in args.cand}
|
|
95
|
+
common = set(teacher) & set(prompts)
|
|
96
|
+
for name in args.cand:
|
|
97
|
+
common &= set(cands[name])
|
|
98
|
+
common = sorted(common)
|
|
99
|
+
report['compared'] = len(common)
|
|
100
|
+
for name in args.cand:
|
|
101
|
+
rows = cands[name]
|
|
102
|
+
exact = sum(1 for i in common if rows[i]['gold'] == teacher[i]['gold'])
|
|
103
|
+
grades = [graded(rows[i]['gold'], rows[i].get('secondary', []),
|
|
104
|
+
teacher[i]['gold'], teacher[i].get('secondary', [])) for i in common]
|
|
105
|
+
scored = [rows[i]['scores'] for i in common]
|
|
106
|
+
dist = {intent: sum(s[intent] for s in scored) / len(scored) for intent in INTENTS}
|
|
107
|
+
top_mean = sum(max(s.values()) for s in scored) / len(scored)
|
|
108
|
+
rank_corr = {intent: spearman([rows[i]['scores'][intent] for i in common],
|
|
109
|
+
[teacher[i]['scores'][intent] for i in common]) for intent in INTENTS}
|
|
110
|
+
slices = {}
|
|
111
|
+
for lang, pred in (('zh', is_zh), ('en', lambda t: not is_zh(t))):
|
|
112
|
+
ids = [i for i in common if pred(prompts[i])]
|
|
113
|
+
if ids:
|
|
114
|
+
slices[lang] = {'n': len(ids),
|
|
115
|
+
'exact': sum(1 for i in ids if rows[i]['gold'] == teacher[i]['gold']) / len(ids),
|
|
116
|
+
'graded': sum(graded(rows[i]['gold'], rows[i].get('secondary', []),
|
|
117
|
+
teacher[i]['gold'], teacher[i].get('secondary', [])) for i in ids) / len(ids)}
|
|
118
|
+
report['candidates'][name] = {
|
|
119
|
+
'sha256': hashlib.sha256((data / name).read_bytes()).hexdigest(),
|
|
120
|
+
'n': len(common), 'primaryExact': exact / len(common),
|
|
121
|
+
'gradedMean': sum(grades) / len(grades),
|
|
122
|
+
'meanTopScore': top_mean, 'meanScoreByIntent': dist,
|
|
123
|
+
'rankCorrByIntent': rank_corr,
|
|
124
|
+
'nullRate': sum(1 for i in common if rows[i]['gold'] is None) / len(common),
|
|
125
|
+
'teacherNullRate': sum(1 for i in common if teacher[i]['gold'] is None) / len(common),
|
|
126
|
+
'slices': slices,
|
|
127
|
+
'routing': {},
|
|
128
|
+
}
|
|
129
|
+
for i in common:
|
|
130
|
+
routed = (rows[i].get('labeler') or {}).get('routing', 'unknown')
|
|
131
|
+
report['candidates'][name]['routing'][routed] = \
|
|
132
|
+
report['candidates'][name]['routing'].get(routed, 0) + 1
|
|
133
|
+
# Backend parity over shared ids.
|
|
134
|
+
if len(args.cand) == 2:
|
|
135
|
+
a, b = (cands[args.cand[0]], cands[args.cand[1]])
|
|
136
|
+
same_route = sum(1 for i in common
|
|
137
|
+
if (a[i].get('labeler') or {}).get('routing') == (b[i].get('labeler') or {}).get('routing'))
|
|
138
|
+
same_gold = sum(1 for i in common if a[i]['gold'] == b[i]['gold'])
|
|
139
|
+
deltas = [max(abs(a[i]['scores'][k] - b[i]['scores'][k]) for k in INTENTS) for i in common]
|
|
140
|
+
report['parity'] = {'pair': list(args.cand), 'n': len(common),
|
|
141
|
+
'routingAgreement': same_route / len(common),
|
|
142
|
+
'derivedGoldAgreement': same_gold / len(common),
|
|
143
|
+
'maxAbsDeltaMean': sum(deltas) / len(deltas),
|
|
144
|
+
'maxAbsDeltaP95': sorted(deltas)[int(len(deltas) * 0.95)]}
|
|
145
|
+
Path(args.out).write_text(json.dumps(report, indent=2), encoding='utf-8')
|
|
146
|
+
print(json.dumps({k: v for k, v in report.items() if k != 'candidates'}, indent=2))
|
|
147
|
+
for name, c in report['candidates'].items():
|
|
148
|
+
print(f"{name}: exact={c['primaryExact']:.3f} graded={c['gradedMean']:.3f} "
|
|
149
|
+
f"top={c['meanTopScore']:.3f} null={c['nullRate']:.3f} routing={c['routing']} slices={c['slices']}")
|
|
150
|
+
if 'parity' in report:
|
|
151
|
+
print('parity:', json.dumps(report['parity']))
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == '__main__':
|
|
156
|
+
sys.exit(main())
|