#!/usr/bin/env python3 """Validate one completed de Vos return envelope locally; no network or file writes.""" import csv,datetime,hashlib,io,json,math,re,sys from pathlib import Path TASK='devos-1954-tungsten-curve20-independent-review' ARCHIVE='a477aa3b21a79031a72b971c335ea31107690dda1c998aa7739e8a164eb94001' MEMBER='19872fe822f646bf2a223b9366b9d37d4ff051de77fcc4d2d938ade98702b4ff' HEADER=['source_row','wavelength_um','emittance','source_page','transcription_note'] REQUIRED={'schema_version','task_slug','reviewer_role','prior_exposure','method_origin','known_connection','source_worksheet_sha256','sealed_at_utc','source_locator','source_row_count','archive_sha256','member_sha256','comparison_method','agreements','discrepancies','uncertainty','rights_statement','attachment_sha256s','curator_fields','instructions'} HEX=re.compile(r'^[0-9a-f]{64}$');UTC_SECOND=re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$') MAX_BYTES=65536;MAX_ROWS=200;MAX_NOTE=500 def stop(reason): print(json.dumps({'schema_version':'openlab-devos-return-check-v2','status':'rejected','reason':reason,'network_requests':0,'files_written':0},separators=(',',':')));return 1 def worksheet_rows(raw): try:text=raw.decode('utf-8-sig') except UnicodeDecodeError:return None,'worksheet must be UTF-8' if '\x00' in text:return None,'worksheet contains a NUL byte' try: reader=csv.DictReader(io.StringIO(text,newline='')) if reader.fieldnames!=HEADER:return None,'exact five-column worksheet header required' rows=list(reader) except csv.Error:return None,'malformed worksheet CSV' if not 1<=len(rows)<=MAX_ROWS:return None,'worksheet must contain 1..200 data rows' 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 None,f'worksheet row {i}: wrong column count' if any(('\r' in v or '\n' in v) for v in row.values()):return None,f'worksheet row {i}: embedded line break refused' try:source_row=int(row['source_row']) except ValueError:return None,f'worksheet row {i}: source_row must be an integer' if source_row<=0 or source_row in seen_rows or source_row<=previous:return None,f'worksheet 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 None,f'worksheet row {i}: wavelength and emittance must be decimal numbers' if not math.isfinite(wavelength) or not 050:return None,f'worksheet row {i}: source_page required and limited to 50 characters' if len(row['transcription_note'])>MAX_NOTE:return None,f'worksheet row {i}: transcription_note exceeds 500 characters' return rows,None def main(a): if len(a)!=3:return stop('usage: validate-devos-return.py FILLED.csv COMPLETED-RETURN.json') wp,rp=map(Path,a[1:]) if not wp.is_file() or not rp.is_file():return stop('both local files are required') wb=wp.read_bytes();rb=rp.read_bytes() if not 1<=len(wb)<=MAX_BYTES or not 1<=len(rb)<=MAX_BYTES:return stop('file size outside 1..65536 bytes') rows,worksheet_error=worksheet_rows(wb) if worksheet_error:return stop(worksheet_error) try:data=json.loads(rb.decode('utf-8')) except (UnicodeDecodeError,json.JSONDecodeError):return stop('valid UTF-8 return JSON required') if not isinstance(data,dict) or set(data)!=REQUIRED:return stop('exact return-template fields required') if data['schema_version']!='openlab-devos-offline-return-v1' or data['task_slug']!=TASK:return stop('schema or task binding mismatch') actual=hashlib.sha256(wb).hexdigest() if data['source_worksheet_sha256']!=actual:return stop('worksheet SHA-256 mismatch') if data['source_row_count']!=len(rows):return stop('source_row_count mismatch') if data['reviewer_role'] not in ['source_reader','independent_reproducer']:return stop('reviewer_role invalid') if data['prior_exposure'] not in ['task_only','saw_result','unsure']:return stop('prior_exposure invalid') if data['method_origin'] not in ['independent','kiwerb_reference','unsure']:return stop('method_origin invalid') if data['known_connection'] not in ['none_known','known_connection','unsure']:return stop('known_connection invalid') if not isinstance(data['sealed_at_utc'],str) or not UTC_SECOND.fullmatch(data['sealed_at_utc']):return stop('sealed_at_utc must be whole-second UTC ending Z') try: sealed=datetime.datetime.fromisoformat(data['sealed_at_utc'].replace('Z','+00:00')) if sealed.isoformat(timespec='seconds').replace('+00:00','Z')!=data['sealed_at_utc']:raise ValueError except ValueError:return stop('sealed_at_utc must be a real whole-second UTC time') locator=data['source_locator'] if not isinstance(locator,dict) or set(locator)!={'tprc_volume','table','curve','source_pages_used'} or locator['tprc_volume']!=7 or locator['table']!=234 or locator['curve']!=20:return stop('source locator incomplete or changed') pages=locator['source_pages_used'] if not isinstance(pages,list) or not 1<=len(pages)<=20 or any(not isinstance(x,str) or not x.strip() or len(x)>50 for x in pages):return stop('source_pages_used must contain 1..20 bounded text locators') if data['archive_sha256']!=ARCHIVE or data['member_sha256']!=MEMBER:return stop('archive binding mismatch') for key in ['comparison_method','agreements','discrepancies','uncertainty','rights_statement']: if not isinstance(data[key],str) or not data[key].strip() or len(data[key])>10000:return stop(f'{key} must be nonempty bounded text') if not isinstance(data['attachment_sha256s'],list) or len(data['attachment_sha256s'])>20 or any(not isinstance(x,str) or not HEX.fullmatch(x) for x in data['attachment_sha256s']) or len(set(data['attachment_sha256s']))!=len(data['attachment_sha256s']):return stop('attachment hashes invalid') if data['curator_fields']!={'received_at_utc':None,'identity_status':'not_reviewed','independence_status':'not_reviewed','scientific_status':'not_reviewed'}:return stop('participant must not change curator fields') if not isinstance(data['instructions'],list):return stop('template instructions missing') out={'schema_version':'openlab-devos-return-check-v2','status':'structurally_ready_for_manual_route_review_not_admitted','worksheet_sha256':actual,'return_sha256':hashlib.sha256(rb).hexdigest(),'source_rows':len(rows),'reviewer_role':data['reviewer_role'],'prior_exposure':data['prior_exposure'],'method_origin':data['method_origin'],'known_connection':data['known_connection'],'attachment_hash_count':len(data['attachment_sha256s']),'curator_fields_unchanged':True,'worksheet_full_structure_rechecked':True,'sealed_at_whole_second_utc':True,'answer_values_used':False,'expected_outcome_used':False,'network_requests':0,'files_written':0,'limits':'Structure and bindings only; no identity, independence, source truth, scientific validation, acceptance, publication, correction or discovery.'};print(json.dumps(out,sort_keys=True,separators=(',',':')));return 0 if __name__=='__main__':raise SystemExit(main(sys.argv))