harness-everything 0.29.0 → 0.30.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +35 -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/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/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/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
|
@@ -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())
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
// Assembles
|
|
4
|
-
// See docs/system-one-corpus.md. A decision counts only for the exact prompt text it reviewed.
|
|
3
|
+
// Assembles a System One holdout (tier or intent stage) from a draft plus human review decisions.
|
|
4
|
+
// See docs/system-one-corpus.md and docs/system-one-intent.md. A decision counts only for the exact prompt text it reviewed.
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const { createHash } = require('node:crypto');
|
|
7
7
|
const { createRequest } = require('../harness-everything/scripts/system-one/contract');
|
|
8
|
-
const {
|
|
8
|
+
const { CATALOGS, MULTI, goldLabels, validSecondary } = require('../harness-everything/scripts/system-one/catalogs');
|
|
9
9
|
const { validateCorpus } = require('../harness-everything/scripts/system-one/evaluate');
|
|
10
10
|
|
|
11
11
|
const SOURCES = ['synthetic:authored', 'derived:local-history'];
|
|
12
12
|
const LANGUAGES = ['en', 'zh-TW'];
|
|
13
|
-
const GOLD = ['tier1', 'tier2', 'tier3', null];
|
|
14
13
|
const REVIEWER = 'human:repository-owner';
|
|
15
14
|
// Drafts are committed publicly: no email addresses or user home paths.
|
|
16
15
|
const PRIVATE = [/[\w.+-]+@[\w-]+\.[\w.-]+/, /[A-Za-z]:\\Users\\/i, /\/home\/[^/\s]+/, /\/Users\/[^/\s]+/];
|
|
@@ -20,7 +19,8 @@ const nonempty = v => typeof v === 'string' && v.trim().length > 0;
|
|
|
20
19
|
const fail = code => { throw new TypeError(code); };
|
|
21
20
|
const promptHash = text => createHash('sha256').update(text, 'utf8').digest('hex');
|
|
22
21
|
|
|
23
|
-
function validateDraft(draft) {
|
|
22
|
+
function validateDraft(draft, task = 'tier') {
|
|
23
|
+
const GOLD = goldLabels(task);
|
|
24
24
|
if (!exact(draft, ['schemaVersion', 'cases']) || draft.schemaVersion !== 1 || !Array.isArray(draft.cases) || !draft.cases.length) fail('invalid-draft');
|
|
25
25
|
const ids = new Set(); const prompts = new Set();
|
|
26
26
|
for (const c of draft.cases) {
|
|
@@ -33,24 +33,27 @@ function validateDraft(draft) {
|
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function validateReviews(reviews) {
|
|
36
|
+
function validateReviews(reviews, task = 'tier') {
|
|
37
|
+
const GOLD = goldLabels(task);
|
|
37
38
|
if (!exact(reviews, ['schemaVersion', 'decisions']) || reviews.schemaVersion !== 1 || !Array.isArray(reviews.decisions)) fail('invalid-reviews');
|
|
38
39
|
const ids = new Set();
|
|
39
40
|
for (const d of reviews.decisions) {
|
|
40
|
-
|
|
41
|
+
const multi = MULTI.includes(task);
|
|
42
|
+
if (!exact(d, ['id', 'promptHash', 'decision', 'gold', ...(multi ? ['secondary'] : []), 'reviewer']) || !nonempty(d.id) || ids.has(d.id)
|
|
41
43
|
|| typeof d.promptHash !== 'string' || !/^[0-9a-f]{64}$/.test(d.promptHash)
|
|
42
44
|
|| !['accept', 'relabel', 'reject'].includes(d.decision) || !GOLD.includes(d.gold) || d.reviewer !== REVIEWER
|
|
45
|
+
|| !validSecondary(task, d.gold, d.secondary)
|
|
43
46
|
|| (d.decision === 'reject' && d.gold !== null)) fail('invalid-reviews');
|
|
44
47
|
ids.add(d.id);
|
|
45
48
|
}
|
|
46
49
|
return true;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
function build(draft, reviews) {
|
|
50
|
-
validateDraft(draft);
|
|
52
|
+
function build(draft, reviews, task = 'tier') {
|
|
53
|
+
validateDraft(draft, task);
|
|
51
54
|
const byId = new Map();
|
|
52
55
|
if (reviews !== null) {
|
|
53
|
-
validateReviews(reviews);
|
|
56
|
+
validateReviews(reviews, task);
|
|
54
57
|
for (const d of reviews.decisions) {
|
|
55
58
|
const c = draft.cases.find(item => item.id === d.id);
|
|
56
59
|
if (!c) fail('unknown-review-case');
|
|
@@ -60,7 +63,8 @@ function build(draft, reviews) {
|
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
const summary = { cases: 0, reviewed: 0, rejected: 0, stale: 0, unreviewed: 0,
|
|
63
|
-
byLanguage: { en: 0, 'zh-TW': 0 }, byGold:
|
|
66
|
+
byLanguage: { en: 0, 'zh-TW': 0 }, byGold: Object.fromEntries(CATALOGS[task].map(o => [o.id, 0])),
|
|
67
|
+
...(MULTI.includes(task) ? { bySecondary: {} } : {}), reviewedHoldoutGate: false };
|
|
64
68
|
const cases = [];
|
|
65
69
|
for (const c of draft.cases) {
|
|
66
70
|
const d = byId.get(c.id);
|
|
@@ -70,10 +74,13 @@ function build(draft, reviews) {
|
|
|
70
74
|
const reviewed = Boolean(current);
|
|
71
75
|
if (reviewed) summary.reviewed += 1; else if (!d) summary.unreviewed += 1;
|
|
72
76
|
const gold = reviewed ? d.gold : c.proposedGold;
|
|
77
|
+
// Drafts propose only the primary intent; secondary intents come from the reviewer.
|
|
78
|
+
const secondary = MULTI.includes(task) ? { secondary: reviewed ? [...d.secondary] : [] } : {};
|
|
73
79
|
cases.push({ id: c.id, family: c.family, split: 'holdout', language: c.language, source: c.source, reviewed,
|
|
74
|
-
request: createRequest(
|
|
80
|
+
request: createRequest(task, c.prompt, CATALOGS[task]), gold, ...secondary });
|
|
75
81
|
summary.byLanguage[c.language] += 1;
|
|
76
82
|
summary.byGold[gold === null ? 'unclassified' : gold] += 1;
|
|
83
|
+
for (const s of secondary.secondary || []) summary.bySecondary[s] = (summary.bySecondary[s] || 0) + 1;
|
|
77
84
|
}
|
|
78
85
|
summary.cases = cases.length;
|
|
79
86
|
const corpus = { schemaVersion: 1, cases };
|
|
@@ -85,14 +92,14 @@ function build(draft, reviews) {
|
|
|
85
92
|
|
|
86
93
|
if (require.main === module) {
|
|
87
94
|
try {
|
|
88
|
-
const [op, draftPath, reviewsPath, outPath, extra] = process.argv.slice(2);
|
|
95
|
+
const [op, draftPath, reviewsPath, outPath, task = 'tier', extra] = process.argv.slice(2);
|
|
89
96
|
if (op !== 'build' || !draftPath || !reviewsPath || !outPath || extra) throw new Error('usage');
|
|
90
97
|
const read = file => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
91
|
-
const { corpus, summary } = build(read(draftPath), reviewsPath === '-' ? null : read(reviewsPath));
|
|
98
|
+
const { corpus, summary } = build(read(draftPath), reviewsPath === '-' ? null : read(reviewsPath), task);
|
|
92
99
|
fs.writeFileSync(outPath, `${JSON.stringify(corpus, null, 2)}\n`);
|
|
93
100
|
console.log(JSON.stringify(summary));
|
|
94
101
|
} catch (_) {
|
|
95
|
-
console.error('Usage: system-one-corpus.js build <draft.json> <reviews.json|-> <corpus.json> (invalid draft, reviews, or path)');
|
|
102
|
+
console.error('Usage: system-one-corpus.js build <draft.json> <reviews.json|-> <corpus.json> [tier|intent] (invalid draft, reviews, or path)');
|
|
96
103
|
process.exitCode = 1;
|
|
97
104
|
}
|
|
98
105
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Research evaluation driver for the fine-tuned Laya bridge (#255 Phase 3).
|
|
4
|
+
// It does NOT touch provider.js/resident.js (still cua-only): it starts the
|
|
5
|
+
// laya resident server itself, scores cases through it, and feeds the
|
|
6
|
+
// production decide()/evaluate() machinery. Usage:
|
|
7
|
+
// score <cases.json> <manifest.json> <records.json> [workdir]
|
|
8
|
+
// report <corpus.json> <records.json> <report.json> [--secondary-threshold T]
|
|
9
|
+
// cases.json: { cases: [{ id, baseline, request }] } (request = holdout-style,
|
|
10
|
+
// context + 13 catalog options). records.json matches evaluate() input.
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const net = require('node:net');
|
|
14
|
+
const { spawn } = require('node:child_process');
|
|
15
|
+
const { performance } = require('node:perf_hooks');
|
|
16
|
+
const { evaluate } = require('../harness-everything/scripts/system-one/evaluate');
|
|
17
|
+
const { decide } = require('../harness-everything/scripts/system-one/contract');
|
|
18
|
+
const { decisionPolicy } = require('../harness-everything/scripts/system-one/provider');
|
|
19
|
+
|
|
20
|
+
const ADAPTER = path.join(__dirname, '..', 'harness-everything', 'scripts', 'system-one', 'laya_adapter.py');
|
|
21
|
+
const CALL_TIMEOUT_MS = 30000;
|
|
22
|
+
|
|
23
|
+
function send(port, token, message) {
|
|
24
|
+
return new Promise(resolve => {
|
|
25
|
+
const socket = net.connect({ port, host: '127.0.0.1' });
|
|
26
|
+
const chunks = [];
|
|
27
|
+
const done = value => { socket.destroy(); resolve(value); };
|
|
28
|
+
const timer = setTimeout(() => done({ ok: false, reason: 'timeout' }), CALL_TIMEOUT_MS);
|
|
29
|
+
socket.on('connect', () => socket.end(`${JSON.stringify({ token, ...message })}\n`));
|
|
30
|
+
socket.on('data', c => chunks.push(c));
|
|
31
|
+
socket.on('end', () => { clearTimeout(timer);
|
|
32
|
+
try { done(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
|
|
33
|
+
catch (_) { done({ ok: false, reason: 'bad-reply' }); } });
|
|
34
|
+
socket.on('error', () => { clearTimeout(timer); done({ ok: false, reason: 'io' }); });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function withServer(python, manifest, workdir, fn) {
|
|
39
|
+
const manifestJson = JSON.stringify(manifest);
|
|
40
|
+
const state = path.join(workdir, 'laya-research-state.json');
|
|
41
|
+
const lock = path.join(workdir, 'laya-research-state.starting');
|
|
42
|
+
for (const f of [state, lock]) try { fs.unlinkSync(f); } catch (_) { /* fresh */ }
|
|
43
|
+
const child = spawn(python, [ADAPTER, '--serve', manifestJson, state, lock], { stdio: 'ignore' });
|
|
44
|
+
const deadline = Date.now() + 120000;
|
|
45
|
+
let info = null;
|
|
46
|
+
while (Date.now() < deadline) {
|
|
47
|
+
try { info = JSON.parse(fs.readFileSync(state, 'utf8')); if (info.port && info.token) break; } catch (_) { /* starting */ }
|
|
48
|
+
if (child.exitCode !== null) throw new Error(`server exited during startup (code ${child.exitCode})`);
|
|
49
|
+
await new Promise(r => setTimeout(r, 200));
|
|
50
|
+
info = null;
|
|
51
|
+
}
|
|
52
|
+
if (!info) { child.kill('SIGKILL'); throw new Error('server did not become ready'); }
|
|
53
|
+
try {
|
|
54
|
+
return await fn(info);
|
|
55
|
+
} finally {
|
|
56
|
+
try { await send(info.port, info.token, { op: 'shutdown' }); } catch (_) { /* exiting */ }
|
|
57
|
+
child.kill('SIGKILL');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function score(casesPath, manifestPath, recordsPath, workdir) {
|
|
62
|
+
const { cases } = JSON.parse(fs.readFileSync(casesPath, 'utf8'));
|
|
63
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
64
|
+
const policy = decisionPolicy(manifest.acceptance);
|
|
65
|
+
const records = await withServer(manifest.python, manifest, workdir, async info => {
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const c of cases) {
|
|
68
|
+
const runs = [];
|
|
69
|
+
for (let i = 0; i < 2; i++) {
|
|
70
|
+
const start = performance.now();
|
|
71
|
+
const reply = await send(info.port, info.token, { op: 'score', request: c.request });
|
|
72
|
+
const latencyMs = performance.now() - start;
|
|
73
|
+
let decision;
|
|
74
|
+
let scores = null;
|
|
75
|
+
if (reply.ok) {
|
|
76
|
+
decision = decide(c.request, reply.response, policy);
|
|
77
|
+
if (decision.selectedId === 'unclassified') {
|
|
78
|
+
decision = { ...decision, status: 'abstain', reason: 'unclassified', selectedId: null };
|
|
79
|
+
}
|
|
80
|
+
if (['accepted', 'abstain'].includes(decision.status)) scores = reply.response.scores.map(s => s.probability);
|
|
81
|
+
} else {
|
|
82
|
+
decision = { status: 'unavailable', reason: reply.reason };
|
|
83
|
+
}
|
|
84
|
+
runs.push({ decision, scores, latencyMs, coldStart: false });
|
|
85
|
+
}
|
|
86
|
+
out.push({ id: c.id, baseline: c.baseline, runs });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
});
|
|
90
|
+
fs.writeFileSync(recordsPath, `${JSON.stringify(records)}\n`);
|
|
91
|
+
console.log(JSON.stringify({ cases: records.length }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function report(corpusPath, recordsPath, reportPath, secondaryThreshold) {
|
|
95
|
+
const corpus = JSON.parse(fs.readFileSync(corpusPath, 'utf8'));
|
|
96
|
+
const records = JSON.parse(fs.readFileSync(recordsPath, 'utf8'));
|
|
97
|
+
const result = evaluate(corpus, records, null, { secondaryThreshold });
|
|
98
|
+
fs.writeFileSync(reportPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
99
|
+
console.log(JSON.stringify({ cases: result.cases, coverage: result.model.coverage,
|
|
100
|
+
acceptedPrecision: result.model.acceptedPrecision, rolloutReady: result.rolloutReady }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function main() {
|
|
104
|
+
const [mode, a, b, c, d] = process.argv.slice(2);
|
|
105
|
+
if (mode === 'score' && a && b && c) return score(a, b, c, d || fs.mkdtempSync('/tmp/laya-eval-'));
|
|
106
|
+
if (mode === 'report' && a && b && c) {
|
|
107
|
+
const t = d === undefined ? undefined : Number((d.match(/^--secondary-threshold=(.+)$/) || [])[1]);
|
|
108
|
+
return report(a, b, c, t);
|
|
109
|
+
}
|
|
110
|
+
throw new Error('usage: system-one-eval-laya.js (score <cases> <manifest> <records> [workdir] | report <corpus> <records> <report> [--secondary-threshold=T])');
|
|
111
|
+
}
|
|
112
|
+
main().catch(err => { console.error(`laya research eval failed: ${err.message}`); process.exitCode = 1; });
|