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.
Files changed (88) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +1 -0
  4. package/CHANGELOG.md +35 -0
  5. package/PRIVACY.md +9 -0
  6. package/benchmarks/fixtures/system-one-intent-holdout-draft.json +2228 -0
  7. package/benchmarks/fixtures/system-one-intent-holdout.json +16509 -0
  8. package/benchmarks/fixtures/system-one-intent-reviews.json +2723 -0
  9. package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/README.md +64 -0
  10. package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/report.json +15177 -0
  11. package/benchmarks/results/system-one/labeler-holdout-check-intent-2026-09-24/report.json +2976 -0
  12. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-005-steps.json +6125 -0
  13. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-haiku-sonnet-escalation.json +6344 -0
  14. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-scaled-margin-escalation.json +6206 -0
  15. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report.json +6103 -0
  16. package/benchmarks/results/system-one/laya-ft-2026-09-25/README.md +83 -0
  17. package/benchmarks/results/system-one/laya-ft-2026-09-25/report.json +71 -0
  18. package/benchmarks/results/system-one/laya-ft-2026-09-25/shadow-report.json +44 -0
  19. package/benchmarks/results/system-one/laya-zs-2026-09-25/README.md +68 -0
  20. package/benchmarks/results/system-one/laya-zs-2026-09-25/report.json +173 -0
  21. package/ci/mechanism-34-observations.test.js +550 -0
  22. package/ci/mechanism-system-one-collect.test.js +16 -0
  23. package/ci/mechanism-system-one-intent.test.js +406 -0
  24. package/ci/mechanism-system-one-laya.test.js +45 -0
  25. package/ci/mechanism-system-one-relevance.test.js +112 -0
  26. package/ci/mechanism-system-one-resident.test.js +16 -0
  27. package/ci/system-one-laya-calibrate-test.py +65 -0
  28. package/ci/system-one-laya-export-test.py +155 -0
  29. package/ci/system-one-laya-label-test.py +201 -0
  30. package/ci/system-one-laya-perintent-test.py +60 -0
  31. package/ci/system-one-laya-provider-test.py +115 -0
  32. package/ci/system-one-laya-train-test.py +93 -0
  33. package/docs/routing.md +25 -0
  34. package/docs/system-one-intent-gates.md +102 -0
  35. package/docs/system-one-intent.md +173 -0
  36. package/docs/system-one-laya-training.md +38 -0
  37. package/docs/system-one-observations.md +206 -0
  38. package/docs/system-one-routing.md +25 -10
  39. package/docs/system-one-suggestion-gates.md +292 -0
  40. package/docs/system-one-training.md +56 -6
  41. package/harness-everything/SKILL.md +1 -1
  42. package/harness-everything/scripts/kernel-router.js +10 -0
  43. package/harness-everything/scripts/system-one/catalogs.js +35 -0
  44. package/harness-everything/scripts/system-one/contract.js +45 -1
  45. package/harness-everything/scripts/system-one/evaluate.js +115 -19
  46. package/harness-everything/scripts/system-one/intent.js +22 -0
  47. package/harness-everything/scripts/system-one/laya_adapter.py +141 -0
  48. package/harness-everything/scripts/system-one/provider.js +10 -3
  49. package/harness-everything/scripts/system-one/resident.js +8 -3
  50. package/harness-everything/scripts/system-one/router.js +1 -1
  51. package/hooks/hooks.json +40 -0
  52. package/hooks/scripts/codex-user-prompt.js +39 -0
  53. package/hooks/scripts/lib/observations.js +581 -0
  54. package/hooks/scripts/observation-hook.js +25 -0
  55. package/opencode-plugin/plugin.json +1 -1
  56. package/package.json +1 -1
  57. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  58. package/plugins/harness-everything/hooks/hooks.json +35 -2
  59. package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +39 -0
  60. package/plugins/harness-everything/hooks/scripts/lib/observations.js +581 -0
  61. package/plugins/harness-everything/hooks/scripts/observation-hook.js +25 -0
  62. package/plugins/harness-everything/plugin.json +1 -1
  63. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  64. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +10 -0
  65. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/catalogs.js +35 -0
  66. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/contract.js +45 -1
  67. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/evaluate.js +115 -19
  68. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/intent.js +22 -0
  69. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/laya_adapter.py +141 -0
  70. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/provider.js +10 -3
  71. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/resident.js +8 -3
  72. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/router.js +1 -1
  73. package/sbom.cdx.json +7 -7
  74. package/scripts/evaluate-system-one.js +17 -5
  75. package/scripts/system-one-calibrate-laya.py +165 -0
  76. package/scripts/system-one-calibrate.js +42 -13
  77. package/scripts/system-one-collect.js +7 -3
  78. package/scripts/system-one-compare-laya.py +156 -0
  79. package/scripts/system-one-corpus.js +22 -15
  80. package/scripts/system-one-eval-laya.js +112 -0
  81. package/scripts/system-one-export-laya.py +197 -0
  82. package/scripts/system-one-label-laya.py +243 -0
  83. package/scripts/system-one-label.js +163 -37
  84. package/scripts/system-one-observations-export.js +106 -0
  85. package/scripts/system-one-observations-review.js +86 -0
  86. package/scripts/system-one-perintent-laya.py +134 -0
  87. package/scripts/system-one-train-laya.py +344 -0
  88. package/scripts/system-one-train-ngram.py +66 -14
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env python3
2
+ """Export soft-target RLCD training items for Laya fine-tuning (#255 Phase 3).
3
+
4
+ Each train prompt with teacher relevance scores becomes 12 `noul` items, one
5
+ per owner intent (catalog order, v1 wording shared with system-one-label-laya).
6
+ Teacher relevance p maps to gold probabilities {true: p, false: 1-p}, matching
7
+ the upstream fine-tune notebook (LocalLLaMA/typed-decisions, noul target
8
+ [false, true], qtype 2, label = argmax).
9
+
10
+ Output is reviewable jsonl (one item per line); the training host converts it
11
+ to train_items.pt with torch.save (never commit .pt). Sequence building goes
12
+ through laya.common.build_sequence so tokenization matches inference exactly.
13
+ Stdlib only at import time; `laya` is imported lazily inside the default
14
+ builder so contract tests run without model weights.
15
+ """
16
+ import argparse
17
+ import hashlib
18
+ import importlib.util
19
+ import json
20
+ import random
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ HERE = Path(__file__).resolve()
25
+ _label_spec = importlib.util.spec_from_file_location(
26
+ 'laya_labeler', HERE.parent / 'system-one-label-laya.py')
27
+ _label = importlib.util.module_from_spec(_label_spec)
28
+ _label_spec.loader.exec_module(_label)
29
+ INTENTS = _label.INTENTS
30
+ QUESTIONS_VERSION = _label.QUESTIONS_VERSION
31
+ build_questions = _label.build_questions
32
+
33
+ # laya.common.QTYPES = {"choice": 0, "score": 1, "noul": 2}.
34
+ QTYPE_NOUL = 2
35
+
36
+
37
+ def build_item(state_text, instructions, probs, builder):
38
+ """One upstream-format training item from gold {true, false} probabilities."""
39
+ pf, pt = float(probs['false']), float(probs['true'])
40
+ if not (pf >= 0 and pt >= 0 and abs(pf + pt - 1.0) < 1e-6):
41
+ raise ValueError(f'probabilities must sum to 1, got {probs!r}')
42
+ built = builder(state_text, instructions, {'true': pt, 'false': pf})
43
+ if built is None:
44
+ return None
45
+ target = [built['target'][0], built['target'][1]]
46
+ if abs(sum(target) - 1.0) >= 1e-6:
47
+ raise ValueError(f'builder target must sum to 1, got {target!r}')
48
+ return {'ids': built['ids'], 'markers': built['markers'], 'qtype': QTYPE_NOUL,
49
+ 'target': target, 'label': target.index(max(target))}
50
+
51
+
52
+ def parse_oversample(spec):
53
+ """Parse 'intent:factor,...' into {intent: factor}; factor 1 is rejected."""
54
+ table = {}
55
+ for part in (spec or '').split(','):
56
+ if not part.strip():
57
+ continue
58
+ try:
59
+ intent, factor = part.split(':')
60
+ factor = int(factor)
61
+ except ValueError:
62
+ raise ValueError(f'oversample: bad entry {part!r}, want intent:factor')
63
+ if intent not in INTENTS or factor < 2:
64
+ raise ValueError(f'oversample: bad entry {part!r}, want known intent and factor >= 2')
65
+ table[intent] = factor
66
+ return table
67
+
68
+
69
+ def default_builder_factory(checkpoint='convaiinnovations/laya', subfolder='multilingual'):
70
+ """Real sequence builder through laya.common (lazy import)."""
71
+ from huggingface_hub import snapshot_download # noqa: PLC0415
72
+ from transformers import AutoTokenizer # noqa: PLC0415
73
+ from laya.agent import _fix_tokenizer_config # noqa: PLC0415
74
+ from laya.common import build_sequence, render_options # noqa: PLC0415
75
+
76
+ model_dir = Path(snapshot_download(checkpoint))
77
+ revision = model_dir.name
78
+ base = model_dir / subfolder if subfolder else model_dir
79
+ _fix_tokenizer_config(str(base))
80
+ tok = AutoTokenizer.from_pretrained(str(base / 'tokenizer'))
81
+ cfg = json.loads((base / 'rl_agent_config.json').read_text(encoding='utf-8'))
82
+ questions = build_questions()
83
+
84
+ def builder(state_text, instructions, probs):
85
+ question = {'t': 'noul', 'ins': instructions, 'crit': {}}
86
+ seq, markers = build_sequence(tok, state_text, question, cfg['max_len'], cfg['head_max_len'])
87
+ if len(markers) != len(render_options(question)):
88
+ return None
89
+ return {'ids': seq, 'markers': markers,
90
+ 'target': [probs['false'], probs['true']]}
91
+
92
+ provenance = {'checkpoint': checkpoint, 'revision': revision, 'subfolder': subfolder or 'root',
93
+ 'questions': QUESTIONS_VERSION,
94
+ 'tokenizerDir': str(base / 'tokenizer')}
95
+ return builder, provenance
96
+
97
+
98
+ def export(data_dir, split, out_path, manifest_path, builder, seed=0, forbid_path=None,
99
+ teacher_file='labels-intent-scores.jsonl', provenance=None, oversample=None):
100
+ """Export one prompts.jsonl split to upstream-format items."""
101
+ if split not in ('train', 'validation'):
102
+ raise ValueError(f'split must be train or validation, got {split!r}')
103
+ oversample = dict(oversample or {})
104
+ for intent, factor in oversample.items():
105
+ if intent not in INTENTS or not isinstance(factor, int) or factor < 2:
106
+ raise ValueError(f'oversample: bad entry {intent!r}:{factor!r}')
107
+ data_dir, out_path, manifest_path = Path(data_dir), Path(out_path), Path(manifest_path)
108
+ prompts_raw = (data_dir / 'prompts.jsonl').read_text(encoding='utf-8')
109
+ teacher_raw = (data_dir / teacher_file).read_text(encoding='utf-8')
110
+ teacher = {r['id']: r for r in
111
+ (json.loads(line) for line in teacher_raw.splitlines() if line.strip())}
112
+ rows = sorted((r for r in (json.loads(line) for line in prompts_raw.splitlines() if line.strip())
113
+ if r.get('split') == split), key=lambda r: r['id'])
114
+ if forbid_path is not None:
115
+ forbidden = set(json.loads(Path(forbid_path).read_text(encoding='utf-8')))
116
+ clash = sorted({r['id'] for r in rows} & forbidden)
117
+ if clash:
118
+ raise ValueError(f'overlap with forbidden ids: {clash[:5]} (n={len(clash)})')
119
+ if seed:
120
+ random.Random(seed).shuffle(rows)
121
+ questions = build_questions()
122
+ items, skipped_no_teacher, skipped_markers = 0, 0, 0
123
+ with out_path.open('w', encoding='utf-8') as fh:
124
+ for prompt in rows:
125
+ trow = teacher.get(prompt['id'])
126
+ if trow is None:
127
+ skipped_no_teacher += 1
128
+ continue
129
+ for intent in INTENTS:
130
+ p = float(trow['scores'].get(intent, 0.0))
131
+ repeat = oversample.get(intent, 1) if p >= 0.4 else 1
132
+ item = build_item(prompt['text'], questions[intent]['instructions'],
133
+ {'true': p, 'false': 1.0 - p}, builder)
134
+ if item is None:
135
+ skipped_markers += repeat
136
+ continue
137
+ line = json.dumps({'promptId': prompt['id'], 'intent': intent, **item}) + '\n'
138
+ for _ in range(repeat):
139
+ fh.write(line)
140
+ items += repeat
141
+ manifest = {'split': split, 'seed': seed, 'questions': QUESTIONS_VERSION,
142
+ 'builder': provenance or {'builder': 'injected'},
143
+ 'oversample': oversample,
144
+ 'items': items, 'prompts': len(rows),
145
+ 'skippedNoTeacher': skipped_no_teacher, 'skippedMarkers': skipped_markers,
146
+ 'promptsSha256': hashlib.sha256(prompts_raw.encode('utf-8')).hexdigest(),
147
+ 'teacherSha256': hashlib.sha256(teacher_raw.encode('utf-8')).hexdigest(),
148
+ 'teacherFile': teacher_file,
149
+ 'outputSha256': hashlib.sha256(out_path.read_bytes()).hexdigest()}
150
+ manifest_path.write_text(json.dumps(manifest, indent=2), encoding='utf-8')
151
+ return {'items': items, 'skippedNoTeacher': skipped_no_teacher,
152
+ 'skippedMarkers': skipped_markers, 'manifest': manifest}
153
+
154
+
155
+ def main(argv=None):
156
+ parser = argparse.ArgumentParser(description=__doc__)
157
+ parser.add_argument('--data-dir', required=True, help='private corpus dir')
158
+ parser.add_argument('--split', choices=['train', 'validation'], default='train')
159
+ parser.add_argument('--out', required=True, help='output items jsonl')
160
+ parser.add_argument('--manifest', required=True, help='provenance manifest json')
161
+ parser.add_argument('--checkpoint', default='convaiinnovations/laya')
162
+ parser.add_argument('--subfolder', default='multilingual')
163
+ parser.add_argument('--seed', type=int, default=0, help='0 keeps id order; nonzero shuffles')
164
+ parser.add_argument('--forbid', default=None, help='json list of forbidden ids (overlap guard)')
165
+ parser.add_argument('--teacher', default='labels-intent-scores.jsonl')
166
+ parser.add_argument('--oversample', default='', help='intent:factor,... to repeat positive intent items')
167
+ parser.add_argument('--limit', type=int, default=0, help='export only the first N prompts (0 = all)')
168
+ args = parser.parse_args(argv)
169
+ oversample = parse_oversample(args.oversample)
170
+ builder, provenance = default_builder_factory(args.checkpoint, args.subfolder)
171
+ print(json.dumps({'builder': provenance}))
172
+ # --limit applies before export by staging a filtered view; export() keeps split semantics.
173
+ if args.limit > 0:
174
+ import tempfile # noqa: PLC0415
175
+ data_dir = Path(args.data_dir)
176
+ prompts_raw = (data_dir / 'prompts.jsonl').read_text(encoding='utf-8')
177
+ rows = [json.loads(line) for line in prompts_raw.splitlines() if line.strip()]
178
+ todo = sorted((r for r in rows if r.get('split') == args.split), key=lambda r: r['id'])[:args.limit]
179
+ with tempfile.TemporaryDirectory() as tmp:
180
+ view = Path(tmp) / 'view'
181
+ view.mkdir()
182
+ (view / 'prompts.jsonl').write_text(
183
+ '\n'.join(json.dumps(r) for r in todo) + '\n', encoding='utf-8')
184
+ (view / args.teacher).write_text((data_dir / args.teacher).read_text(encoding='utf-8'), encoding='utf-8')
185
+ summary = export(view, args.split, args.out, args.manifest, builder,
186
+ seed=args.seed, forbid_path=args.forbid, teacher_file=args.teacher,
187
+ provenance=provenance, oversample=oversample)
188
+ else:
189
+ summary = export(args.data_dir, args.split, args.out, args.manifest, builder,
190
+ seed=args.seed, forbid_path=args.forbid, teacher_file=args.teacher,
191
+ provenance=provenance, oversample=oversample)
192
+ print(json.dumps({k: v for k, v in summary.items() if k != 'manifest'}))
193
+ return 0
194
+
195
+
196
+ if __name__ == '__main__':
197
+ sys.exit(main())
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env python3
2
+ """Zero-shot dense intent labeling with Laya (#255 Phase 1+2).
3
+
4
+ Maps each of the 12 owner intents (catalogs.js order, frozen) to one Laya
5
+ `noul` question and records the yes-probability as the per-intent relevance
6
+ score. Output rows match the `labels-intent-scores.jsonl` trainer contract
7
+ {id, gold, secondary, scores}, but MUST be written to a separate file: the
8
+ sonnet-scored labels are never overwritten. Derived gold/secondary are
9
+ mechanical bootstrap only (labeler.derived=true), never owner truth.
10
+
11
+ Stdlib only at import time; `laya` / `laya_mlx` are imported lazily inside
12
+ the backend constructors so contract tests run without model weights.
13
+ """
14
+ import argparse
15
+ import hashlib
16
+ import json
17
+ import math
18
+ import statistics
19
+ import sys
20
+ import time
21
+ from pathlib import Path
22
+
23
+ # Owner catalog order (harness-everything/scripts/system-one/catalogs.js).
24
+ # This order is the scorer output order; changing it invalidates weights.
25
+ INTENTS = ['explain', 'discuss', 'git', 'fix', 'edit', 'feature', 'refactor',
26
+ 'review', 'test', 'docs', 'plan', 'investigate']
27
+
28
+ QUESTIONS_VERSION = 'v1'
29
+
30
+ # The sonnet-scored labels file. The labeler refuses to write to it.
31
+ PROTECTED_OUTPUT = 'labels-intent-scores.jsonl'
32
+
33
+ _PROPOSITIONS = {
34
+ 'explain': 'The request asks for an explanation of how something works or an answer to a question.',
35
+ 'discuss': 'The request asks to weigh options, give an opinion, or settle a decision.',
36
+ 'git': 'The request asks for Git or GitHub housekeeping: commit, push, branch, or pull request.',
37
+ 'fix': 'The request asks to repair wrong behavior: a bug, crash, failing test, or build.',
38
+ 'edit': 'The request asks to change an existing value, setting, text, or behavior.',
39
+ 'feature': 'The request asks to add new behavior: a command, option, format, or script.',
40
+ 'refactor': 'The request asks to restructure or unify existing code without adding new behavior.',
41
+ 'review': 'The request asks to evaluate an existing artifact against a standard.',
42
+ 'test': 'The request asks to run tests or builds, or to write tests as the main deliverable.',
43
+ 'docs': 'The request asks to write or edit documentation as the main deliverable.',
44
+ 'plan': 'The request asks to produce a plan, specification, or breakdown before implementation.',
45
+ 'investigate': 'The request asks to find facts or a cause and report back without changing anything.',
46
+ }
47
+
48
+ # v2 adds contrastive exclusions aimed at the measured top-2 co-firing pairs
49
+ # (edit/refactor, fix/investigate, investigate/review/explain, docs/edit,
50
+ # feature/edit, plan/discuss). Same 12 intents, same semantics: exclusions
51
+ # only sharpen boundaries, they never add or remove a category.
52
+ _PROPOSITIONS_V2 = {
53
+ 'explain': 'The request asks for an explanation of how something works or an answer to a question. Not investigating an unknown cause (investigate).',
54
+ 'discuss': 'The request asks to weigh options, give an opinion, or settle a decision. Not producing an implementation plan or breakdown (plan).',
55
+ 'git': 'The request asks for Git or GitHub housekeeping: commit, push, branch, or pull request.',
56
+ 'fix': 'The request asks to repair wrong behavior: a bug, crash, failing test, or build. Not merely finding the cause (investigate); only when a repair is requested.',
57
+ 'edit': 'The request asks to change an existing value, setting, text, or behavior. Not a bug repair (fix), not a restructuring (refactor), not new behavior (feature), not documentation as the deliverable (docs).',
58
+ 'feature': 'The request asks to add new behavior: a command, option, format, or script. Not modifying existing behavior (edit).',
59
+ 'refactor': 'The request asks to restructure or unify existing code without adding new behavior. Not a value change (edit), not a bug repair (fix).',
60
+ 'review': 'The request asks to evaluate an existing artifact against a standard. Not fact-finding (investigate).',
61
+ 'test': 'The request asks to run tests or builds, or to write tests as the main deliverable.',
62
+ 'docs': 'The request asks to write or edit documentation as the main deliverable. Not incidental text edits alongside other work (edit).',
63
+ 'plan': 'The request asks to produce a plan, specification, or breakdown before implementation. Not weighing options in discussion (discuss).',
64
+ 'investigate': 'The request asks to find facts or a cause and report back without changing anything. Not repairing (fix), not judging quality (review), not explaining how things work (explain).',
65
+ }
66
+
67
+ _QUESTIONS = {'v1': _PROPOSITIONS, 'v2': _PROPOSITIONS_V2}
68
+
69
+
70
+ def build_questions(version='v1'):
71
+ """One `noul` question per intent, in catalog order."""
72
+ if version not in _QUESTIONS:
73
+ raise ValueError(f'unknown questions version: {version!r}')
74
+ table = _QUESTIONS[version]
75
+ return {intent: {'type': 'noul', 'instructions': table[intent]} for intent in INTENTS}
76
+
77
+
78
+ def validate_scores(scores):
79
+ """True when scores cover exactly the 12 intents with finite 0..1 floats."""
80
+ if not isinstance(scores, dict) or set(scores) != set(INTENTS):
81
+ raise ValueError(f'scores keys must be exactly the 12 intents, got {sorted(scores) if isinstance(scores, dict) else type(scores)}')
82
+ for intent in INTENTS:
83
+ v = scores[intent]
84
+ if not isinstance(v, float) or not math.isfinite(v) or not 0.0 <= v <= 1.0:
85
+ raise ValueError(f'scores[{intent!r}] out of range: {v!r}')
86
+ return True
87
+
88
+
89
+ def derive_label(scores):
90
+ """Mechanical {gold, secondary} from bands (bootstrap only, derived=true).
91
+
92
+ Mirrors the scoredLabel bands: secondary is every other intent at 0.4+;
93
+ a max at or below 0.2 abstains to null. Ties break by catalog order.
94
+ """
95
+ validate_scores(scores)
96
+ top = max(scores[intent] for intent in INTENTS)
97
+ if top <= 0.2:
98
+ return {'gold': None, 'secondary': [], 'derived': True}
99
+ gold = max(INTENTS, key=lambda i: (scores[i], -INTENTS.index(i)))
100
+ secondary = sorted((i for i in INTENTS if i != gold and scores[i] >= 0.4),
101
+ key=lambda i: (-scores[i], INTENTS.index(i)))
102
+ return {'gold': gold, 'secondary': secondary, 'derived': True}
103
+
104
+
105
+ def output_row(prompt, scores, backend, out_name, routing=None, questions_version='v1'):
106
+ """One trainer-contract row for a labeled prompt."""
107
+ if Path(out_name).name == PROTECTED_OUTPUT:
108
+ raise ValueError(f'protected output: refusing to overwrite {PROTECTED_OUTPUT}')
109
+ derived = derive_label(scores)
110
+ labeler = {'task': 'intent', 'target': 'scores', 'backend': backend,
111
+ 'questions': questions_version, 'derived': True}
112
+ if routing is not None:
113
+ labeler['routing'] = routing
114
+ return {'id': prompt['id'], 'gold': derived['gold'], 'secondary': derived['secondary'],
115
+ 'scores': {intent: scores[intent] for intent in INTENTS},
116
+ 'labeler': labeler}
117
+
118
+
119
+ def torch_backend(questions_version='v1'):
120
+ """Official PyTorch Laya backend (lazy import). Returns (fn, provenance)."""
121
+ import laya # noqa: PLC0415
122
+ router = laya.Router()
123
+ questions = build_questions(questions_version)
124
+
125
+ def run(text):
126
+ started = time.monotonic()
127
+ result = router.predict(text, questions)
128
+ elapsed_ms = (time.monotonic() - started) * 1000.0
129
+ answers = result['answers']
130
+ scores = {intent: float(answers[intent]['noul']) for intent in INTENTS}
131
+ return scores, result['routing']['model'], elapsed_ms
132
+
133
+ provenance = {'package': 'laya', 'version': laya.__version__, 'questions': questions_version}
134
+ return run, provenance
135
+
136
+
137
+ def mlx_backend(questions_version='v1'):
138
+ """Native MLX Laya port backend (lazy import, inference only)."""
139
+ import laya_mlx # noqa: PLC0415
140
+ from laya_mlx import Router # noqa: PLC0415
141
+ router = Router()
142
+ questions = build_questions(questions_version)
143
+
144
+ def run(text):
145
+ started = time.monotonic()
146
+ result = router.predict(text, questions)
147
+ elapsed_ms = (time.monotonic() - started) * 1000.0
148
+ answers = result['answers']
149
+ scores = {intent: float(answers[intent]['noul']) for intent in INTENTS}
150
+ return scores, result['routing']['model'], elapsed_ms
151
+
152
+ provenance = {'package': 'laya-mlx', 'version': laya_mlx.__version__, 'questions': questions_version}
153
+ return run, provenance
154
+
155
+
156
+ def label_rows(rows, split, input_sha, out_path, manifest_path, backend_fn, backend_name, resume=False,
157
+ questions_version='v1'):
158
+ """Label explicit rows; returns a summary dict."""
159
+ out_path, manifest_path = Path(out_path), Path(manifest_path)
160
+ if out_path.name == PROTECTED_OUTPUT:
161
+ raise ValueError(f'protected output: refusing to overwrite {PROTECTED_OUTPUT}')
162
+ done = set()
163
+ if resume and out_path.exists():
164
+ for line in out_path.read_text(encoding='utf-8').splitlines():
165
+ if line.strip():
166
+ done.add(json.loads(line)['id'])
167
+ rows = [r for r in rows if r['id'] not in done]
168
+ routing, latencies, this_run = {}, [], 0
169
+ with out_path.open('a' if (resume and done) else 'w', encoding='utf-8') as fh:
170
+ for prompt in rows:
171
+ scores, routed, elapsed_ms = backend_fn(prompt['text'])
172
+ fh.write(json.dumps(output_row(prompt, scores, backend_name, out_path.name, routed,
173
+ questions_version)) + '\n')
174
+ routing[routed] = routing.get(routed, 0) + 1
175
+ latencies.append(elapsed_ms)
176
+ this_run += 1
177
+ total, full_routing = this_run, routing
178
+ if resume:
179
+ # The file is the source of truth: recount everything for cumulative totals.
180
+ full_routing = {}
181
+ total = 0
182
+ for line in out_path.read_text(encoding='utf-8').splitlines():
183
+ if not line.strip():
184
+ continue
185
+ row = json.loads(line)
186
+ routed = (row.get('labeler') or {}).get('routing', 'unknown')
187
+ full_routing[routed] = full_routing.get(routed, 0) + 1
188
+ total += 1
189
+ manifest = {'backend': backend_name, 'split': split, 'questions': questions_version,
190
+ 'labeled': total, 'thisRun': this_run, 'resumed': bool(resume and done),
191
+ 'inputSha256': input_sha, 'routing': full_routing,
192
+ 'latencyMs': {'p50': statistics.median(latencies) if latencies else 0.0,
193
+ 'p95': statistics.quantiles(latencies, n=100)[94] if len(latencies) >= 100 else (max(latencies) if latencies else 0.0)}}
194
+ manifest_path.write_text(json.dumps(manifest, indent=2), encoding='utf-8')
195
+ return manifest
196
+
197
+
198
+ def label_split(data_dir, split, out_path, manifest_path, backend_fn, backend_name, questions_version='v1'):
199
+ """Label one prompts.jsonl split; returns a summary dict."""
200
+ if split not in ('train', 'validation'):
201
+ raise ValueError(f'split must be train or validation, got {split!r}')
202
+ data_dir = Path(data_dir)
203
+ prompts_raw = (data_dir / 'prompts.jsonl').read_text(encoding='utf-8')
204
+ rows = [json.loads(line) for line in prompts_raw.splitlines() if line.strip()]
205
+ todo = [r for r in rows if r.get('split') == split]
206
+ return label_rows(todo, split, hashlib.sha256(prompts_raw.encode('utf-8')).hexdigest(),
207
+ out_path, manifest_path, backend_fn, backend_name,
208
+ questions_version=questions_version)
209
+
210
+
211
+ def main(argv=None):
212
+ parser = argparse.ArgumentParser(description=__doc__)
213
+ parser.add_argument('--data-dir', required=True, help='private corpus dir (holds prompts.jsonl)')
214
+ parser.add_argument('--split', choices=['train', 'validation'], default='validation')
215
+ parser.add_argument('--out', required=True, help='output jsonl (never labels-intent-scores.jsonl)')
216
+ parser.add_argument('--manifest', required=True, help='provenance manifest json')
217
+ parser.add_argument('--backend', choices=['torch', 'mlx'], required=True)
218
+ parser.add_argument('--questions', choices=['v1', 'v2'], default='v1')
219
+ parser.add_argument('--limit', type=int, default=0, help='label only the first N rows (0 = all)')
220
+ parser.add_argument('--resume', action='store_true', help='skip ids already present in --out')
221
+ args = parser.parse_args(argv)
222
+ run, provenance = (torch_backend(args.questions) if args.backend == 'torch'
223
+ else mlx_backend(args.questions))
224
+ print(json.dumps({'backend': args.backend, 'provenance': provenance}))
225
+
226
+ data_dir = Path(args.data_dir)
227
+ prompts_raw = (data_dir / 'prompts.jsonl').read_text(encoding='utf-8')
228
+ rows = [json.loads(line) for line in prompts_raw.splitlines() if line.strip()]
229
+ todo = [r for r in rows if r.get('split') == args.split]
230
+ if args.limit > 0:
231
+ todo = todo[:args.limit]
232
+ summary = label_rows(todo, args.split,
233
+ hashlib.sha256(prompts_raw.encode('utf-8')).hexdigest(),
234
+ args.out, args.manifest, run, args.backend, resume=args.resume,
235
+ questions_version=args.questions)
236
+ summary['limit'] = args.limit
237
+ Path(args.manifest).write_text(json.dumps(summary, indent=2), encoding='utf-8')
238
+ print(json.dumps(summary))
239
+ return 0
240
+
241
+
242
+ if __name__ == '__main__':
243
+ sys.exit(main())