@skill-harness/cli 0.25.0 → 0.25.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/dist/decision.js +35 -11
- package/package.json +4 -4
package/dist/decision.js
CHANGED
|
@@ -253,10 +253,20 @@ def ordinary(path):
|
|
|
253
253
|
def closed(value, fields, name):
|
|
254
254
|
require(isinstance(value, dict) and set(value) == set(fields), name + ' has unsupported or missing fields')
|
|
255
255
|
|
|
256
|
+
def jsonl_rows(path):
|
|
257
|
+
# JSONL records end only at LF. Unicode NEL/LS/PS are legal JSON string data.
|
|
258
|
+
lines = ordinary(path).decode('utf-8').split('\n')
|
|
259
|
+
if lines[-1] == '': lines.pop()
|
|
260
|
+
return [json.loads(line) for line in lines]
|
|
261
|
+
|
|
256
262
|
def export_data(root):
|
|
257
263
|
root = pathlib.Path(root).absolute()
|
|
258
264
|
raw = ordinary(root / 'export-manifest.json'); m = json.loads(raw)
|
|
259
|
-
require(m.get('schema') ==
|
|
265
|
+
require(m.get('schema') == 2 and m.get('kind') == 'decision-learning-export', 'Unsupported export schema; re-export with the current skill-harness')
|
|
266
|
+
normalization = m['inputNormalization']
|
|
267
|
+
closed(normalization, ['algorithm','runtime','nodeVersion','unicodeVersion'], 'Input normalization producer')
|
|
268
|
+
require(normalization['algorithm'] == 'ecmascript-nfkc-whitespace-trim-lower-sha256-v1' and normalization['runtime'] == 'node', 'Unsupported input normalization producer')
|
|
269
|
+
require(all(isinstance(normalization[key], str) and re.fullmatch(r'[0-9]+(?:\.[0-9]+){1,2}', normalization[key]) for key in ['nodeVersion','unicodeVersion']), 'Invalid input normalization producer version')
|
|
260
270
|
require(m.get('trainingExecuted') is False and m.get('providerPredictionsIncluded') is False, 'Unsupported source provenance')
|
|
261
271
|
require(set(m['files']) == {'train.jsonl','validation.jsonl','test.jsonl','train-lora.py','requirements-training.txt','training-config.example.json','TRAINING.md'}, 'Unexpected export files')
|
|
262
272
|
for name, ref in m['files'].items():
|
|
@@ -266,10 +276,10 @@ def export_data(root):
|
|
|
266
276
|
require(digest(ordinary(pathlib.Path(__file__).absolute())) == m['files']['train-lora.py']['sha256'], 'Run the exact exported training script')
|
|
267
277
|
groups, seen_inputs, seen_cases = {}, {}, set()
|
|
268
278
|
for split in ['train','validation','test']:
|
|
269
|
-
rows =
|
|
279
|
+
rows = jsonl_rows(root / (split+'.jsonl'))
|
|
270
280
|
require(len(rows) == m['counts'][split], 'Split count mismatch')
|
|
271
281
|
for row in rows:
|
|
272
|
-
closed(row, ['caseId','caseHash','taskGroup','lineageGroup','sessionId','fixtureOnly','input','question','answer','label','source'], 'Dataset row')
|
|
282
|
+
closed(row, ['caseId','caseHash','taskGroup','lineageGroup','sessionId','fixtureOnly','input','inputIdentity','question','answer','label','source'], 'Dataset row')
|
|
273
283
|
require(row['caseId'] not in seen_cases and type(row['answer']) is bool, 'Duplicate case or invalid label')
|
|
274
284
|
seen_cases.add(row['caseId'])
|
|
275
285
|
require(row['label']['caseId'] == row['caseId'] and row['label']['caseHash'] == row['caseHash'] and row['label']['value'] == row['answer'] and row['label']['kind'] in ['human','test'] and row['label']['independent'] is True, 'Label identity mismatch')
|
|
@@ -278,7 +288,15 @@ def export_data(root):
|
|
|
278
288
|
identity = key + ':' + row[key]
|
|
279
289
|
require(identity not in groups or groups[identity] == split, 'Related cases cross splits')
|
|
280
290
|
groups[identity] = split
|
|
281
|
-
|
|
291
|
+
identity = row['inputIdentity']
|
|
292
|
+
closed(identity, ['algorithm','inputSha256','normalizedSha256'], 'Input identity receipt')
|
|
293
|
+
require(identity['algorithm'] == normalization['algorithm'], 'Input identity algorithm mismatch')
|
|
294
|
+
require(all(isinstance(identity[key], str) and re.fullmatch(r'[0-9a-f]{64}', identity[key]) for key in ['inputSha256','normalizedSha256']), 'Invalid input identity digest')
|
|
295
|
+
require(isinstance(row['input'], str) and digest(row['input'].encode('utf-8')) == identity['inputSha256'], 'Input identity byte mismatch')
|
|
296
|
+
# This is producer evidence bound by the exact row/file/manifest hashes.
|
|
297
|
+
# Recomputing with Python Unicode tables can disagree with the Node producer.
|
|
298
|
+
# The receipt is not an independent proof that normalization was correct.
|
|
299
|
+
normalized = identity['normalizedSha256']
|
|
282
300
|
require(normalized not in seen_inputs or seen_inputs[normalized] == split, 'Duplicate input crosses splits')
|
|
283
301
|
seen_inputs[normalized] = split
|
|
284
302
|
if m.get('trainingEligible') is True: require(row['fixtureOnly'] is False and row['sessionId'] is not None, 'Fixture/anonymous data cannot be training eligible')
|
|
@@ -353,8 +371,7 @@ def main():
|
|
|
353
371
|
model = get_peft_model(model, LoraConfig(task_type=TaskType.CAUSAL_LM, r=t['loraRank'], lora_alpha=t['loraAlpha'], target_modules=t['targetModules'], lora_dropout=0.0, bias='none'))
|
|
354
372
|
def rows(split):
|
|
355
373
|
data = []
|
|
356
|
-
for
|
|
357
|
-
row = json.loads(line)
|
|
374
|
+
for row in jsonl_rows(root / (split + '.jsonl')):
|
|
358
375
|
require(row['fixtureOnly'] is False and type(row['answer']) is bool and row['label']['kind'] in ['human','test'] and row['label']['independent'] is True, 'Only independently labeled reviewed real data may train')
|
|
359
376
|
prompt = 'Question: ' + row['question'] + '\nEvidence:\n' + row['input'] + '\nAnswer (true or false): '
|
|
360
377
|
prefix = tokenizer.encode(prompt, add_special_tokens=True)
|
|
@@ -384,6 +401,8 @@ var TRAINING_DOC = `# Local LoRA workflow
|
|
|
384
401
|
|
|
385
402
|
This export contains independent labels, never JEV predictions. A fixture demonstration is not training eligible. No training or installation has been performed by exporting these files.
|
|
386
403
|
|
|
404
|
+
Export schema 2 records the producer's normalized-input SHA-256 receipt in every row, the exact input UTF-8 SHA-256, and the Node/Unicode versions and normalization algorithm in the manifest. One JavaScript normalizer validates the split and writes the receipt. Python checks receipt shape, raw-input binding, duplicate identities and exact file hashes; it does not reinterpret those identities with its own Unicode tables. These are trusted producer receipts, not independent proofs of correct normalization. Separate training approval still pins the exact export manifest. Invalid Unicode scalar input is refused by the producer. Re-export older schema-1 packets with the current CLI; do not modify their bound script, rows or manifest in place.
|
|
405
|
+
|
|
387
406
|
1. Check the intact export offline: python3 train-lora.py --export /absolute/export --check-export-only.
|
|
388
407
|
2. For reviewed real data only, select a base model, immutable full revision, license and an existing canonical local weight directory. Pin every file by SHA-256 in a separate copy of training-config.example.json. Select architecture-specific LoRA target modules. Record an explicit eligibility review bound to the exact export-manifest.json digest. Storage consent alone is insufficient.
|
|
389
408
|
3. Use an isolated Python 3.12 environment with the exact requirements-training.txt versions. Resolve dependencies separately, retain the complete resolved environment/wheel hashes, and assess the host memory requirements. The template pins direct versions; it is not a platform-complete dependency lock or proof of hardware support.
|
|
@@ -459,6 +478,11 @@ function consentFor(consents, sessionId) {
|
|
|
459
478
|
check(c.decision === "granted", "session storage declined; advice remains allowed");
|
|
460
479
|
return c;
|
|
461
480
|
}
|
|
481
|
+
var INPUT_NORMALIZATION_ALGORITHM = "ecmascript-nfkc-whitespace-trim-lower-sha256-v1";
|
|
482
|
+
function inputIdentity(input) {
|
|
483
|
+
check(!/[\uD800-\uDFFF]/u.test(input), "learning input must contain valid Unicode scalar values");
|
|
484
|
+
return { algorithm: INPUT_NORMALIZATION_ALGORITHM, inputSha256: learningDigest(input), normalizedSha256: learningDigest(input.normalize("NFKC").replace(/\s+/gu, " ").trim().toLowerCase()) };
|
|
485
|
+
}
|
|
462
486
|
var ENTRY_KEYS = ["caseId", "caseHash", "taskGroup", "lineageGroup", "split", "sessionId", "fixtureOnly", "decisionTimeReviewed", "redactionReviewed", "rights", "exportApproved", "trainingApproved", "reviewer"];
|
|
463
487
|
function parseEntry(c, e) {
|
|
464
488
|
keys(e, ENTRY_KEYS, "experiment entry");
|
|
@@ -490,7 +514,7 @@ function parseExperiment(cases, manifest) {
|
|
|
490
514
|
bind(groups, "task:" + e.taskGroup, e.split, "task group");
|
|
491
515
|
bind(groups, "lineage:" + e.lineageGroup, e.split, "lineage group");
|
|
492
516
|
if (e.sessionId !== null) bind(groups, "session:" + e.sessionId, e.split, "session");
|
|
493
|
-
bind(duplicates,
|
|
517
|
+
bind(duplicates, inputIdentity(cases[i].input).normalizedSha256, e.split, "duplicate input");
|
|
494
518
|
bind(duplicates, "source:" + cases[i].source.sha256 + ":" + cases[i].source.recordId, e.split, "source record");
|
|
495
519
|
});
|
|
496
520
|
return { ...structuredClone(manifest), entries };
|
|
@@ -660,7 +684,7 @@ async function prepareExport({ cases, labels, manifest, consents, labelEvidence,
|
|
|
660
684
|
excluded.push({ caseId: c.id, reasons });
|
|
661
685
|
continue;
|
|
662
686
|
}
|
|
663
|
-
const row = { caseId: c.id, caseHash: c.hash, taskGroup: e.taskGroup, lineageGroup: e.lineageGroup, sessionId: e.sessionId, fixtureOnly: e.fixtureOnly, input: c.input, question: c.question, answer: label.label.value, label: { ...label.label }, source: { ...c.source } };
|
|
687
|
+
const row = { caseId: c.id, caseHash: c.hash, taskGroup: e.taskGroup, lineageGroup: e.lineageGroup, sessionId: e.sessionId, fixtureOnly: e.fixtureOnly, input: c.input, inputIdentity: inputIdentity(c.input), question: c.question, answer: label.label.value, label: { ...label.label }, source: { ...c.source } };
|
|
664
688
|
splitRows[e.split].push(row);
|
|
665
689
|
included.push(e);
|
|
666
690
|
}
|
|
@@ -668,16 +692,16 @@ async function prepareExport({ cases, labels, manifest, consents, labelEvidence,
|
|
|
668
692
|
const trainingEligible = mode === "reviewed-data" && excluded.length === 0 && Object.values(splitRows).every((rows) => rows.length > 0) && included.every((e) => !e.fixtureOnly && e.trainingApproved && e.rights === "local-training");
|
|
669
693
|
const files = { ...trainingAssets() };
|
|
670
694
|
for (const [split, rows] of Object.entries(splitRows)) files[`${split}.jsonl`] = rows.map((r) => JSON.stringify(r) + "\n").join("");
|
|
671
|
-
const metadata2 = { schema:
|
|
695
|
+
const metadata2 = { schema: 2, kind: "decision-learning-export", inputNormalization: { algorithm: INPUT_NORMALIZATION_ALGORITHM, runtime: "node", nodeVersion: process.versions.node, unicodeVersion: process.versions.unicode }, mode, experimentId: manifest.id, experimentHash: learningDigest(manifest), trainingEligible, trainingExecuted: false, providerPredictionsIncluded: false, counts: Object.fromEntries(Object.entries(splitRows).map(([s, r]) => [s, r.length])), excluded, review: { frozenAt: manifest.frozenAt, eligibilityRecords: included, independentLabelReceipts: reviewed.reviewed.map((r) => ({ ...r.reference, recordedAt: r.recordedAt })), sessionConsents: [...new Set(included.filter((e) => e.sessionId !== null).map((e) => e.sessionId))].map((sessionId) => {
|
|
672
696
|
const c = consentFor(consents, sessionId);
|
|
673
697
|
return { sessionId, sha256: learningDigest(c), decision: c.decision, interactionId: c.interactionId, recordedAt: c.recordedAt };
|
|
674
698
|
}) }, files: Object.fromEntries(Object.entries(files).map(([name, s]) => [name, { sha256: learningDigest(s), bytes: Buffer.byteLength(s) }])) };
|
|
675
699
|
files["export-manifest.json"] = JSON.stringify(metadata2, null, 2) + "\n";
|
|
676
|
-
return { schema:
|
|
700
|
+
return { schema: 2, kind: "prepared-decision-learning-export", metadata: metadata2, files };
|
|
677
701
|
}
|
|
678
702
|
async function writePreparedExport({ prepared, directory }) {
|
|
679
703
|
keys(prepared, ["schema", "kind", "metadata", "files"], "prepared export");
|
|
680
|
-
check(prepared.schema ===
|
|
704
|
+
check(prepared.schema === 2 && prepared.metadata?.schema === 2 && prepared.kind === "prepared-decision-learning-export", "unsupported prepared export");
|
|
681
705
|
text(directory, "output directory", 4096);
|
|
682
706
|
check(isAbsolute(directory), "output directory must be absolute");
|
|
683
707
|
const allowed = ["train.jsonl", "validation.jsonl", "test.jsonl", "export-manifest.json", "train-lora.py", "training-config.example.json", "requirements-training.txt", "TRAINING.md"];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skill-harness/cli",
|
|
3
|
-
"version": "0.25.
|
|
4
|
-
"description": "skill-harness CLI
|
|
3
|
+
"version": "0.25.1",
|
|
4
|
+
"description": "skill-harness CLI \u2014 run, grade, review, and lint agent-skill scenarios on the pi harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "./dist/cli.js",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"prepack": "node ../../scripts/release-pack.mjs --guard"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@skill-harness/core": "0.25.
|
|
48
|
-
"@skill-harness/adapters": "0.25.
|
|
47
|
+
"@skill-harness/core": "0.25.1",
|
|
48
|
+
"@skill-harness/adapters": "0.25.1"
|
|
49
49
|
}
|
|
50
50
|
}
|