@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.
Files changed (2) hide show
  1. package/dist/decision.js +35 -11
  2. 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') == 1 and m.get('kind') == 'decision-learning-export', 'Unsupported export')
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 = [json.loads(line) for line in ordinary(root / (split+'.jsonl')).decode('utf-8').splitlines()]
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
- normalized = ' '.join(row['input'].casefold().split())
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 line in ordinary(root / (split + '.jsonl')).decode('utf-8').splitlines():
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, learningDigest(cases[i].input.normalize("NFKC").replace(/\s+/gu, " ").trim().toLowerCase()), e.split, "duplicate input");
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: 1, kind: "decision-learning-export", 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) => {
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: 1, kind: "prepared-decision-learning-export", metadata: metadata2, files };
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 === 1 && prepared.kind === "prepared-decision-learning-export", "unsupported prepared export");
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.0",
4
- "description": "skill-harness CLI — run, grade, review, and lint agent-skill scenarios on the pi harness",
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.0",
48
- "@skill-harness/adapters": "0.25.0"
47
+ "@skill-harness/core": "0.25.1",
48
+ "@skill-harness/adapters": "0.25.1"
49
49
  }
50
50
  }