"""Reproduce a typed, source-pinned 32-observation CSV; never fetches or rewrites inputs.

Usage: python3 reproduce-creator-earnings-evidence-view-v1.py ORIGINAL.json CONTEXT.json
       ... ORIGINAL.json CONTEXT.json --dictionary
       ... ORIGINAL.json CONTEXT.json --verify VIEW.csv

Null is \\N; a literal leading backslash in text is doubled. Boolean cells are
true/false. Numeric cells are JSON numeric literals without unit conversion.
Context JSON columns are annotations, never replacements for original fields.
Original normalization may be cited with attribution; source rights remain.
"""
from pathlib import Path
import argparse,csv,hashlib,io,json,sys
SOURCE_SHA='d4d7771bf47ac2166a6ae6998e9b0c4a636080e026fb2da0409acc5973a3dd73'
CONTEXT_SHA='ca7a001546207d85b49e5f7077fc85d270abbd1865a6d833451b82e455ac51f7'
VIEW_ID='creator-earnings-evidence-view-2026-v1'
EXTRAS=['source_row','source_record_url','original_dataset_id','original_dataset_version','original_research_cutoff','original_sha256','supplement_sha256','supplemental_context_json','source_contexts_json']
NULL='\\N'
def load(source,context):
 a=Path(source).read_bytes();b=Path(context).read_bytes()
 if hashlib.sha256(a).hexdigest()!=SOURCE_SHA or hashlib.sha256(b).hexdigest()!=CONTEXT_SHA:raise ValueError('Exact version1 original and context files required')
 d=json.loads(a);c=json.loads(b);assert len(d['records'])==32 and c['original_sha256']==SOURCE_SHA
 keys=list(d['records'][0]);assert len(keys)==34 and all(list(r)==keys for r in d['records'])
 return d,c

def field_schema(d):
 out=[]
 for name in d['records'][0]:
  values=[r[name]for r in d['records']];types={type(v).__name__ for v in values if v is not None}
  if not types:kind='null-only'
  elif types<={'int','float'}:kind='number'
  elif types=={'bool'}:kind='boolean'
  elif types=={'str'}:kind='text'
  else:raise ValueError('Unsupported scalar type '+name)
  out.append(dict(name=name,type=kind,nullable=any(v is None for v in values),source_selector='records[row].'+name,meaning=meaning(name)))
 return out

def meaning(name):
 special={
 'record_id':'Stable original observation ID; one row is a metric, not a creator.',
 'publication_date':'Original stored source-publication date. A later report-release annotation does not replace it.',
 'data_period_start':'Stored start of the measurement period; unknown remains null.',
 'data_period_end':'Stored end of the measurement period; survey collection and income period can differ.',
 'sample_size':'Stored sample. Source-wide and platform-specific populations differ; inspect context and population_description.',
 'mean_or_median':'Stored statistic classification. Means, shares and formulas are not interchangeable.',
 'actual_or_modeled':'Stored evidence-mode label; not an independent accuracy certification.',
 'gross_or_net':'Stored amount basis; unknown is not converted to net income.',
 'platform':'Original platform label; null does not establish a platform-specific observation.',
 'self_reported':'Original normalized boolean flag, not a new verification decision.',
 'transactional':'Original normalized boolean flag, not a guarantee about a wider population.',
 'value_low':'Original value_low scalar; retained without interpreting it as a comparable range across metrics.',
 'value_mid':'Original value_mid scalar; never computed from other cells.',
 'value_high':'Original value_high scalar; retained without interpreting it as a comparable range across metrics.',
 'value_exact':'Original value_exact scalar; read with metric, unit, statistic and basis.',
 'measurement_basis':'What the source observation measures; preserve alongside numeric fields.',
 'source_notes':'Original text; genuine empty strings remain distinct from null.',
 'follower_min':'Original follower_min field; not inferred from other values.',
 'follower_max':'Original follower_max field; not inferred from other values.'}
 return special.get(name,'Original '+name+' field; unchanged, not inferred or normalized again.')

def encode(v):
 if v is None:return NULL
 if isinstance(v,bool):return 'true'if v else'false'
 if isinstance(v,(int,float)):return json.dumps(v,allow_nan=False)
 if not isinstance(v,str):raise ValueError('Only scalar raw fields supported')
 if v.lstrip().startswith(('=','+','-','@')):raise ValueError('Formula-leading text requires a new reviewed export convention')
 return '\\'+v if v.startswith('\\')else v

def decode(cell,kind):
 if cell==NULL:return None
 if kind=='boolean':
  if cell not in ['true','false']:raise ValueError('Invalid boolean')
  return cell=='true'
 if kind=='number':
  v=json.loads(cell)
  if type(v)not in [int,float]:raise ValueError('Invalid number')
  return v
 if kind=='text':return cell[1:]if cell.startswith('\\\\')else cell
 raise ValueError('Non-null value in null-only field')

def export(d,c):
 fields=field_schema(d);keys=[f['name']for f in fields];out=io.StringIO(newline='');writer=csv.DictWriter(out,fieldnames=keys+EXTRAS,lineterminator='\n');writer.writeheader()
 for i,r in enumerate(d['records'],1):
  ctx=c['record_context'].get(r['record_id'],{});refs={k:c['source_contexts'][k]for k in ctx.get('source_context_ids',[])}
  row={k:encode(r[k])for k in keys};row.update(source_row=str(i),source_record_url=d['canonical_page']+'#record-'+r['record_id'],original_dataset_id=d['dataset_id'],original_dataset_version=d['version'],original_research_cutoff=d['research_cutoff'],original_sha256=SOURCE_SHA,supplement_sha256=CONTEXT_SHA,supplemental_context_json=json.dumps(ctx,ensure_ascii=False,separators=(',',':')),source_contexts_json=json.dumps(refs,ensure_ascii=False,separators=(',',':')));writer.writerow(row)
 return out.getvalue()

def verify(text,d,c):
 fields=field_schema(d);reader=csv.DictReader(io.StringIO(text,newline=''));assert reader.fieldnames==[f['name']for f in fields]+EXTRAS;rows=list(reader);assert len(rows)==32
 restored=[{f['name']:decode(row[f['name']],f['type'])for f in fields}for row in rows];assert restored==d['records']
 for before,after in zip(restored,d['records']):
  assert all(type(before[k])is type(after[k])for k in before)
 expected=list(csv.DictReader(io.StringIO(export(d,c),newline='')));assert rows==expected
 return dict(result='PASS',raw_records=32,raw_fields_per_record=34,typed_raw_cells=1088,annotations_separate=True)

def dictionary(d,c,text):
 return dict(view_id=VIEW_ID,view_version='1.0.0',record_count=32,unit_of_analysis=d['unit_of_analysis'],original_dataset_id=d['dataset_id'],original_dataset_version=d['version'],original_research_cutoff=d['research_cutoff'],original_sha256=SOURCE_SHA,supplement_sha256=CONTEXT_SHA,csv_sha256=hashlib.sha256(text.encode()).hexdigest(),original_data_url=d['download_url'],supplement_data_url='https://joshliljenquist.com/data/creator-earnings-source-context-2026-10-30.json',canonical_page=d['canonical_page'],raw_columns=field_schema(d),additional_columns={k:meaning_extra(k)for k in EXTRAS},encoding=dict(format='UTF-8 CSV; comma delimiter and standard quoting',null=NULL,empty_text='Empty CSV cell in a text column is an original empty string, not null',boolean='true or false',number='JSON numeric literal; no rounding or unit conversion',leading_backslash='A literal leading backslash in raw text is doubled; the decoder removes one',context='Two JSON-object columns retain record annotations and resolve their referenced source contexts',spreadsheet_import='Import IDs, ISO date columns and text as text to avoid spreadsheet auto-conversion. Re-verify edited exports.'),rights=d['license_note'],limits=['One row is a source metric/program claim, not an individual creator or campaign','No pooled income benchmark, source-wide sample treated as platform-specific, or new financial finding','Later modeled-income prose context is not part of these frozen32 observations'])

def meaning_extra(k):
 return {'source_row':'One-based original observation order','source_record_url':'Existing report record fragment; a section citation, not a separate indexed page','original_dataset_id':'Unchanged original dataset_id','original_dataset_version':'Unchanged original version','original_research_cutoff':'Unchanged research_cutoff','original_sha256':'Exact original input SHA256','supplement_sha256':'Exact versioned context input SHA256','supplemental_context_json':'record_context[record_id], or an empty JSON object if absent','source_contexts_json':'Resolved source_contexts entries referenced by this row; not raw source fields'}[k]

if __name__=='__main__':
 p=argparse.ArgumentParser(description=__doc__);p.add_argument('source',type=Path);p.add_argument('context',type=Path);g=p.add_mutually_exclusive_group();g.add_argument('--dictionary',action='store_true');g.add_argument('--verify',type=Path);a=p.parse_args();d,c=load(a.source,a.context);text=export(d,c);verify(text,d,c)
 if a.verify:print(json.dumps(verify(a.verify.read_text(encoding='utf-8'),d,c),sort_keys=True))
 elif a.dictionary:print(json.dumps(dictionary(d,c,text),indent=2,ensure_ascii=False))
 else:sys.stdout.buffer.write(text.encode('utf-8'))
