#!/usr/bin/env python3 """Validate and hash one locally completed de Vos worksheet; performs no network I/O.""" from __future__ import annotations import csv, hashlib, io, json, math, sys from pathlib import Path HEADER=['source_row','wavelength_um','emittance','source_page','transcription_note'] MAX_BYTES=65536; MAX_ROWS=200; MAX_NOTE=500 def fail(message): print(json.dumps({'schema_version':'openlab-devos-worksheet-check-v1','status':'rejected','reason':message,'network_requests':0,'files_written':0},separators=(',',':'))) return 1 def main(argv): if len(argv)!=2: return fail('usage: validate_devos_worksheet.py FILLED.csv') path=Path(argv[1]) if not path.is_file(): return fail('worksheet file not found') raw=path.read_bytes() if not raw or len(raw)>MAX_BYTES: return fail('worksheet size must be 1..65536 bytes') try: text=raw.decode('utf-8-sig') except UnicodeDecodeError: return fail('worksheet must be UTF-8') if '\x00' in text: return fail('NUL byte refused') try: reader=csv.DictReader(io.StringIO(text,newline='')) if reader.fieldnames!=HEADER: return fail('exact five-column header required') rows=list(reader) except csv.Error: return fail('malformed CSV') if not 1<=len(rows)<=MAX_ROWS: return fail('worksheet must contain 1..200 data rows; this is a safety bound, not an expected source count') seen_rows=set(); seen_wavelengths=set(); previous=0 for i,row in enumerate(rows,1): if None in row or any(v is None for v in row.values()): return fail(f'row {i}: wrong column count') if any(('\r' in v or '\n' in v) for v in row.values()): return fail(f'row {i}: embedded line break refused') try: source_row=int(row['source_row']) except ValueError: return fail(f'row {i}: source_row must be an integer') if source_row<=0 or source_row in seen_rows or source_row<=previous: return fail(f'row {i}: source_row must be positive, unique and strictly increasing') seen_rows.add(source_row); previous=source_row try: wavelength=float(row['wavelength_um']); emittance=float(row['emittance']) except ValueError: return fail(f'row {i}: wavelength and emittance must be decimal numbers') if not math.isfinite(wavelength) or not 050: return fail(f'row {i}: source_page required and limited to 50 characters') if len(row['transcription_note'])>MAX_NOTE: return fail(f'row {i}: transcription_note exceeds 500 characters') result={'schema_version':'openlab-devos-worksheet-check-v1','status':'structurally_valid_not_scientifically_checked','worksheet_sha256':hashlib.sha256(raw).hexdigest(),'bytes':len(raw),'rows':len(rows),'first_source_row':int(rows[0]['source_row']),'last_source_row':int(rows[-1]['source_row']),'checks':['exact_header','utf8','bounded_size','row_shape','increasing_unique_source_rows','finite_numeric_values','wavelength_structural_bounds','emittance_0_to_1','unique_wavelengths','source_page_present','bounded_notes'],'answer_values_used':False,'expected_row_count_used':False,'network_requests':0,'files_written':0,'limits':'Structure and byte identity only; does not verify transcription, source truth, independence, identity, comparison, validation or discovery.'} print(json.dumps(result,sort_keys=True,separators=(',',':'))); return 0 if __name__=='__main__': raise SystemExit(main(sys.argv))