#!/usr/bin/env python3 """Offline decision-record structure checker, version 1.2.0. Reads an explicitly selected record and optional guide/manifest JSON files, writes a JSON report to stdout, and makes no network request, evidence lookup, file change or external action. It cannot verify that recorded evidence is true or sufficient. A zero exit status means a consistent record, NOT a passed artifact or permission to act. """ from pathlib import Path import argparse import json import sys import hashlib import re MAX_BYTES = 65536 MAX_TEXT = 4000 VERSION = '1.2.0' _NO_MANIFEST = object() TEXT_FIELDS = ('task', 'scope_and_version', 'current_authorization', 'decision_reason', 'next_authorized_action', 'stop_or_recovery','artifact_revision') CHECK_FIELDS = ('check', 'observed_evidence', 'evidence_reference', 'status') ROOT_FIELDS = set(TEXT_FIELDS) | {'schema_version', 'record_type', 'required_checks', 'decision', 'limitations','method_reference','recommendation','artifact_revision'} RECOMMENDATIONS={'keep','change','skip','investigate'} TYPES = {'blank_template', 'fictional_illustration', 'task_record'} STATUSES = {'pass', 'fail', 'unavailable'} DECISIONS = {'accept', 'reject', 'unknown'} def report_base(): return {'checker_version': VERSION, 'scope': 'Record structure and consistency, plus any explicitly requested local method comparison. Not evidence verification.', 'structure_valid': False, 'record_complete': False, 'decision_consistent': False, 'consistency_valid': False, 'decision_from_recorded_statuses': None, 'errors': [], 'missing_fields': [], 'artifact_verified': False, 'evidence_verified': False, 'action_authorized': False, 'method_reference_verified': False, 'publisher_authenticity_verified': False, 'method_comparison': {'status': 'not_requested', 'matched': None}, 'limitations': ['Reads only explicitly supplied record, guide and manifest paths; never follows record references or URLs.', 'Cannot establish truth, sufficiency, task success, access or authorization.', 'A consistent accept label is not independent verification.']} def assess(record): result = report_base() errors = result['errors'] missing = result['missing_fields'] if not isinstance(record, dict): errors.append('Record must be a JSON object.') return result if set(record) != ROOT_FIELDS: errors.append('Root fields must match schema version 1.1 exactly.') if record.get('schema_version') != '1.1': errors.append('schema_version must be 1.1.') if not isinstance(record.get('record_type'),str) or record.get('record_type') not in TYPES: errors.append('record_type is not supported.') if not isinstance(record.get('decision'),str) or record.get('decision') not in DECISIONS: errors.append('decision must be accept, reject or unknown.') if not isinstance(record.get('recommendation'),str) or record.get('recommendation') not in RECOMMENDATIONS: errors.append('recommendation must be keep, change, skip or investigate.') method=record.get('method_reference') if not isinstance(method,dict) or set(method) != {'guide_id','build_revision','source_sha256'}: errors.append('method_reference must name guide_id, build_revision and source_sha256.') else: for key,val in method.items(): if not isinstance(val,str) or len(val)>MAX_TEXT: errors.append('method_reference.'+key+' must be bounded text.') elif not val.strip(): missing.append('method_reference.'+key) digest=method.get('source_sha256') if isinstance(digest,str) and digest and (len(digest)!=64 or any(c not in '0123456789abcdef' for c in digest)): errors.append('method_reference.source_sha256 must be an empty draft field or a lowercase SHA-256 digest.') for key in TEXT_FIELDS: val = record.get(key) if not isinstance(val, str) or len(val) > MAX_TEXT: errors.append(key + ' must be text no longer than 4000 characters.') elif not val.strip(): missing.append(key) limits = record.get('limitations') if not isinstance(limits, list) or not 1 <= len(limits) <= 20 or any( not isinstance(v, str) or not v.strip() or len(v) > MAX_TEXT for v in limits): errors.append('limitations must contain 1 to 20 nonblank text items, each at most 4000 characters.') checks = record.get('required_checks') if not isinstance(checks, list) or not 1 <= len(checks) <= 100: errors.append('required_checks must contain 1 to 100 check objects.') else: for i, check in enumerate(checks): prefix = 'required_checks[' + str(i) + ']' if not isinstance(check, dict) or set(check) != set(CHECK_FIELDS): errors.append(prefix + ' fields must match the schema exactly.') continue for key in CHECK_FIELDS: val = check.get(key) if not isinstance(val, str) or len(val) > MAX_TEXT: errors.append(prefix + '.' + key + ' must be text no longer than 4000 characters.') elif key != 'status' and not val.strip(): missing.append(prefix + '.' + key) if not isinstance(check.get('status'),str) or check.get('status') not in STATUSES: errors.append(prefix + '.status must be pass, fail or unavailable.') if errors: return result result['structure_valid'] = True statuses = [check['status'] for check in checks] # All status labels need a named check, evidence description and reference. # Missing required fields stay unknown; arbitrary text is not verified proof. supported_fail = any(c['status']=='fail' and all(c[k].strip() for k in ('check','observed_evidence','evidence_reference')) for c in checks) if supported_fail: derived = 'reject' elif missing: derived = 'unknown' elif 'unavailable' in statuses: derived = 'unknown' else: derived = 'accept' result['record_complete'] = not missing result['decision_from_recorded_statuses'] = derived result['decision_consistent'] = record['decision'] == derived result['record_type'] = record['record_type'] if not result['decision_consistent']: errors.append('Declared decision conflicts with the recorded statuses or missing fields.') if record['record_type'] == 'blank_template' and ( record['decision'] != 'unknown' or any(s != 'unavailable' for s in statuses)): errors.append('A blank_template must use unknown with unavailable checks. Use task_record for real observations.') result['decision_consistent'] = False if record['record_type'] == 'fictional_illustration': result['limitations'].append('This is a fictional illustration, not a real task result.') result['consistency_valid']=result['structure_valid'] and result['decision_consistent'] and not errors return result def reject_duplicates(pairs): obj = {} for key, value in pairs: if key in obj: raise ValueError('Duplicate JSON field names are not allowed.') obj[key] = value return obj def load_json_bytes(path): # Reads only the explicitly supplied regular file. Content never chooses paths. if path.is_symlink() or not path.is_file(): raise ValueError('Select an existing regular JSON file, not a symlink or directory.') if path.stat().st_size > MAX_BYTES: raise ValueError('JSON input exceeds the 64 KiB limit.') with path.open('rb') as stream: raw = stream.read(MAX_BYTES + 1) if len(raw) > MAX_BYTES: raise ValueError('JSON input exceeds the 64 KiB limit.') try: obj = json.loads(raw.decode('utf-8'), object_pairs_hook=reject_duplicates, parse_constant=lambda _: (_ for _ in ()).throw(ValueError('Non-finite JSON values are not allowed.'))) except (UnicodeError, json.JSONDecodeError, RecursionError) as exc: raise ValueError('Input is not supported UTF-8 JSON.') from exc return obj, raw def load_record(path): return load_json_bytes(path)[0] def compare_method(record, guide, guide_bytes, manifest=_NO_MANIFEST): """Compare explicit local inputs. Matching is not publisher authentication.""" result = {'status': 'invalid', 'matched': False, 'errors': [], 'scope': 'Recorded method ID, build revision and normalized source digest; optionally supplied guide bytes against the selected manifest.', 'guide_file_sha256': hashlib.sha256(guide_bytes).hexdigest(), 'guide_bytes_match_manifest': None, 'publisher_authenticity_verified': False} errors = result['errors'] if not isinstance(record, dict) or not isinstance(record.get('method_reference'), dict): errors.append('A valid record method reference is required.') return result ref = record['method_reference'] if any(not isinstance(ref.get(k), str) or not ref[k].strip() for k in ('guide_id','build_revision','source_sha256')): result['status']='unavailable';errors.append('The record method reference is incomplete.') return result if not isinstance(guide, dict): errors.append('The supplied guide must be a JSON object.') return result guide_id=guide.get('id') if not isinstance(guide_id,str) or not re.fullmatch(r'[a-z0-9]+(?:-[a-z0-9]+)*',guide_id): errors.append('The supplied guide has no valid stable ID.') return result fields=guide.get('source_fields') required={'slug','title','decision','fit','skip','steps','acceptance','stop','limits','sources'} if not isinstance(fields,list) or not 1<=len(fields)<=64 or any(not isinstance(k,str) for k in fields): result['status']='unavailable';errors.append('The guide does not declare supported normalized source fields. Older guides need a manual comparison; no fields are guessed.') return result if len(set(fields))!=len(fields) or not required.issubset(fields) or any(k not in guide for k in fields) or any(k in fields for k in ['source_sha256','source_fields','id','build_revision']): errors.append('The guide source-field declaration is invalid.') return result try: source={k:guide[k] for k in fields} computed=hashlib.sha256(json.dumps(source,sort_keys=True,separators=(',',':'),ensure_ascii=True,allow_nan=False).encode()).hexdigest() except (TypeError,ValueError,RecursionError): errors.append('The guide source fields cannot be normalized.') return result if guide.get('source_sha256')!=computed:errors.append('Guide source digest does not match its actual normalized content.') if guide.get('slug')!=guide_id:errors.append('Guide source ID conflicts with its published ID.') if ref['guide_id']!=guide_id:errors.append('Record and guide IDs differ.') if ref['build_revision']!=guide.get('build_revision'):errors.append('Record and guide build revisions differ.') if ref['source_sha256']!=computed:errors.append('Record and guide source digests differ.') if manifest is not _NO_MANIFEST: if not isinstance(manifest,dict) or manifest.get('schema_version')!='1.0' or manifest.get('algorithm')!='sha256' or not isinstance(manifest.get('files'),dict): result['status']='invalid';errors.append('The selected reading-pack manifest is not supported.') return result if manifest.get('revision')!=guide.get('build_revision'):errors.append('Guide and selected manifest revisions differ.') item=manifest['files'].get('guides/'+guide_id+'/guide.json') matches=isinstance(item,dict) and item.get('sha256')==result['guide_file_sha256'] and type(item.get('bytes')) is int and item['bytes']==len(guide_bytes) result['guide_bytes_match_manifest']=matches if not matches:errors.append('Supplied guide bytes do not match the selected manifest entry.') result['matched']=not errors result['status']='match' if result['matched'] else 'mismatch' return result def main(argv=None): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('record', type=Path, help='One local UTF-8 JSON record (maximum 64 KiB).') parser.add_argument('--guide',type=Path,help='Optional explicitly selected local guide JSON to compare.') parser.add_argument('--manifest',type=Path,help='Optional local reading-pack manifest; requires --guide.') args = parser.parse_args(argv) if args.manifest and not args.guide:parser.error('--manifest requires --guide') try: record=load_record(args.record) result = assess(record) if args.guide: try: guide,raw=load_json_bytes(args.guide) manifest=load_record(args.manifest) if args.manifest else _NO_MANIFEST result['method_comparison']=compare_method(record,guide,raw,manifest) if result['structure_valid'] else {'status':'unavailable','matched':False,'errors':['The record structure must be valid before comparison.']} except (OSError,ValueError) as exc: result['method_comparison']={'status':'invalid','matched':False,'errors':[str(exc) if isinstance(exc,ValueError) else 'The explicitly selected comparison file could not be read.']} except (OSError, ValueError) as exc: result = report_base() # Do not echo paths or private record contents in diagnostics. result['errors'] = [str(exc) if isinstance(exc, ValueError) else 'The selected file could not be read.'] print(json.dumps(result, indent=2, ensure_ascii=False)) if not result['structure_valid']: return 2 if result['method_comparison']['status']=='invalid':return 2 if result['method_comparison']['status'] in ['mismatch','unavailable']:return 3 if not result['decision_consistent']: return 1 return 0 if __name__ == '__main__': sys.exit(main())