"""Bounded offline STDF V4 replay. No vendor SDK, network, or executable input.""" import base64, binascii, csv, hashlib, html, io, json, math, os, struct, sys, tempfile, zipfile from collections import Counter, defaultdict SCHEMA = 'testlotreplay/v1' VERSION = '1.0.0' MAX_BYTES = 8_000_000 MAX_RECORDS = 100_000 MAX_FILES = 32 MAX_DEPTH = 24 class Invalid(ValueError): pass class Incomplete(ValueError): pass def strict(text): if not isinstance(text, str) or len(text.encode('utf8')) > MAX_BYTES: raise Invalid('JSON size limit') def pairs(items): d = {} for k,v in items: if k in d: raise Invalid('duplicate JSON key: '+k) d[k]=v return d try: value=json.loads(text, object_pairs_hook=pairs, parse_constant=lambda x: (_ for _ in ()).throw(Invalid('nonfinite JSON'))) except (ValueError, RecursionError) as e: raise Invalid(str(e)) validate_tree(value) return value def validate_tree(x, depth=0): if depth > MAX_DEPTH: raise Invalid('nesting limit') if isinstance(x, float) and not math.isfinite(x): raise Invalid('nonfinite number') if isinstance(x, (dict,list)): if len(x)>MAX_RECORDS: raise Invalid('collection limit') for v in (x.values() if isinstance(x,dict) else x): validate_tree(v,depth+1) elif isinstance(x,str) and len(x)>MAX_BYTES: raise Invalid('string limit') elif isinstance(x,int) and not isinstance(x,bool) and abs(x)>2**63-1: raise Invalid('integer limit') def dumps(x): return json.dumps(x, sort_keys=True, ensure_ascii=True, allow_nan=False, separators=(',',':')) def sha(b): return hashlib.sha256(b).hexdigest() def integer(v,name,lo=0,hi=2**31-1): if type(v) is not int or not lo<=v<=hi: raise Invalid(name+' must be a bounded integer') return v def decode64(s): if not isinstance(s,str) or len(s)>MAX_BYTES*4//3+8: raise Invalid('base64 size/type') try: b=base64.b64decode(s,validate=True) except (ValueError,binascii.Error): raise Invalid('invalid base64') if len(b)>MAX_BYTES: raise Invalid('binary size limit') return b class Cursor: def __init__(self,b,endian): self.b=b; self.i=0; self.e=endian def get(self,fmt): n=struct.calcsize(fmt) if self.i+n>len(self.b): raise Incomplete('short record field') v=struct.unpack_from(self.e+fmt,self.b,self.i)[0]; self.i+=n; return v def cn(self): n=self.get('B') if self.i+n>len(self.b): raise Incomplete('short counted text') b=self.b[self.i:self.i+n]; self.i+=n # STDF C*n is ASCII; reject unsupported encodings instead of replacement identity. try: return b.decode('ascii') except UnicodeDecodeError: raise Invalid('non-ASCII STDF text unsupported') def tail_strings(self,n): for _ in range(n): if self.i' if struct.unpack_from(endian+'H',data)[0]!=2: raise Invalid('invalid FAR length') pos=0; count=0; seen_parts={}; ptr_defaults={} while posMAX_RECORDS: raise Invalid('record count limit') start=pos if len(data)-pos<4: unknown('TRUNCATED_HEADER',start); break length,typ,sub=struct.unpack_from(endian+'HBB',data,pos); pos+=4 rec=(typ,sub); offsets.append({'file':filename,'file_order':order,'offset':start,'type':typ,'subtype':sub,'length':length}) if pos+length>len(data): unknown('TRUNCATED_RECORD',start); break b=data[pos:pos+length]; pos+=length; c=Cursor(b,endian) if state['mrr']: raise Invalid('records after MRR') try: if rec==(0,10): if start!=0: raise Invalid('repeated FAR') c.get('B'); c.get('B'); c.end() elif rec==(1,10): if state['mir'] or active or attempts: raise Invalid('duplicate or late MIR') c.get('I'); c.get('I'); c.get('B'); c.get('B'); c.get('B'); c.get('B'); c.get('H'); c.get('B') state['lot']=c.cn(); c.tail_strings(29); state['mir']=True if not state['lot']: unknown('MISSING_LOT',start) elif not state['mir']: raise Invalid('MIR must precede data records') elif rec==(1,20): c.get('I'); c.get('B'); c.tail_strings(2); state['mrr']=True if active: unknown('OPEN_PART_EPISODES_AT_MRR',start) elif rec==(1,30): head=c.get('B'); site=c.get('B'); total=c.get('I'); rest=[] while c.ilen(b): raise Incomplete('short B*n repair data') c.i+=n c.end(); key=(head,site); episode=active.pop(key,None) if flags & 0xe0 or flags & 3 == 3: raise Invalid('invalid PRR reserved/supersession flags') status='UNKNOWN' if flags & 0x14 else ('FAIL' if flags & 0x08 else 'PASS') if status=='UNKNOWN': unknown('ABORTED_OR_INVALID_PART_STATUS',start) if episode is None: unknown('PRR_WITHOUT_PIR',start) group=site_groups.get(key); wafer=wafers.get((head,group)) x=None if x==-32768 else x; y=None if y==-32768 else y identity=None if policy['identity']=='part_id': if pid and state['lot']: identity=dumps([state['lot'],pid]) if flags & 2: unknown('COORDINATE_SUPERSESSION_WITH_PART_ID_POLICY',start) else: if state['lot'] and wafer and x is not None and y is not None: identity=dumps([state['lot'],wafer,x,y]) if flags & 1: unknown('PART_ID_SUPERSESSION_WITH_COORDINATE_POLICY',start) if not identity: unknown('UNRESOLVED_DEVICE_IDENTITY',start) if identity and policy['identity']=='part_id': loc=(wafer,x,y) if identity in seen_parts and seen_parts[identity]!=loc: raise Invalid('part ID has conflicting location') seen_parts[identity]=loc a={'file':filename,'file_order':order,'offset':start,'pir_offset':episode['pir_offset'] if episode else None,'lot':state['lot'],'wafer':wafer,'x':x,'y':y,'device_id':pid,'identity':identity,'head':head,'site':site,'pass_status':status,'hard_bin':None if hb==65535 else hb,'soft_bin':None if sb==65535 else sb,'part_flags':flags,'num_tests':tests,'elapsed_ms':elapsed} attempts.append(a) if episode: for m in episode['measurements']: m['attempt_offset']=start; m['identity']=identity; measurements.append(m) elif rec in ((0,20),(50,10),(50,20),(50,30)): # Audit/program text is retained in the source bytes/offsets; no yield semantics. if rec==(0,20): c.get('I'); c.cn() elif rec in ((50,10),(50,30)): c.cn() c.end() else: unknown('UNSUPPORTED_RECORD_%d_%d'%rec,start) except Incomplete: unknown('TRUNCATED_RECORD_FIELDS',start); break if not state['mir']: unknown('MISSING_MIR') if not state['mrr']: unknown('MISSING_MRR') if active: unknown('OPEN_PART_EPISODES') if wafers: unknown('MISSING_WRR') if not attempts: unknown('NO_OBSERVED_ATTEMPTS') for head,site,total in pcr: observed=sum(1 for a in attempts if (head==255 or a['head']==head) and (site==255 or a['site']==site)) if total!=0xffffffff and total!=observed: unknown('PCR_ATTEMPT_COUNT_MISMATCH') return attempts,offsets,measurements,findings,not findings def _sources(payload): if not isinstance(payload,dict) or payload.get('schema',SCHEMA)!=SCHEMA: raise Invalid('schema must be '+SCHEMA) allowed={'schema','stdf_base64','sources','settings','synthetic','attempts_csv'} if set(payload)-allowed: raise Invalid('unexpected payload fields') settings=payload.get('settings',{'identity':'wafer_coordinates'}) if not isinstance(settings,dict) or set(settings)-{'identity','rotation_degrees'}: raise Invalid('unexpected settings') if settings.get('identity') not in ('part_id','wafer_coordinates'): raise Invalid('settings.identity required: part_id or wafer_coordinates') rotation=settings.get('rotation_degrees') if rotation is not None and (type(rotation) is not int or rotation not in (0,90,180,270)): raise Invalid('rotation must be 0,90,180,270 or absent') policy={'identity':settings['identity'],'rotation_degrees':rotation,'chronology':'ascending explicit file order, then PRR byte offset','first':'first observed attempt including UNKNOWN','final':'last observed attempt including UNKNOWN','unknown':'dependent aggregate denominators withheld','scope':'descriptive observed results; no manufacturing acceptance'} choices=sum(k in payload for k in ('stdf_base64','sources','attempts_csv')) if choices!=1: raise Invalid('supply exactly one STDF input, sources, or normalized CSV') if 'attempts_csv' in payload: return [],policy raw=payload.get('sources',[{'filename':'input.stdf','order':0,'stdf_base64':payload.get('stdf_base64')}]) if not isinstance(raw,list) or not 1<=len(raw)<=MAX_FILES: raise Invalid('1..32 sources required') sources=[]; names=set(); orders=set(); hashes=set(); total=0 for item in raw: if not isinstance(item,dict) or set(item)!={'filename','order','stdf_base64'}: raise Invalid('source schema') name=item['filename']; safe_name(name) order=integer(item['order'],'source order'); data=decode64(item['stdf_base64']); digest=sha(data); total+=len(data) if total>MAX_BYTES: raise Invalid('combined binary size limit') if name in names or order in orders or digest in hashes: raise Invalid('duplicate source filename, order or SHA256') names.add(name); orders.add(order); hashes.add(digest); sources.append((order,name,data,digest)) return sorted(sources),policy def safe_name(name): if not isinstance(name,str) or not name or len(name)>120 or name in ('.','..') or '/' in name or '\\' in name or '\x00' in name: raise Invalid('filenames must be safe basenames') def csv_attempts(text,policy): if not isinstance(text,str) or len(text.encode())>MAX_BYTES: raise Invalid('CSV size limit') fields=['lot','wafer','x','y','device_id','attempt_order','head','site','pass_status','hard_bin','soft_bin'] r=csv.DictReader(io.StringIO(text,newline='')) if r.fieldnames!=fields: raise Invalid('attempts.csv exact unique header/order required') attempts=[]; seen=set(); issues=[] for row in r: if len(attempts)>=MAX_RECORDS or None in row or any(v is None for v in row.values()): raise Invalid('CSV row shape/limit') nums={} for field in ('x','y','attempt_order','head','site','hard_bin','soft_bin'): s=row[field] if field in ('x','y','hard_bin','soft_bin') and s=='': nums[field]=None; continue if not s or not s.lstrip('-').isdigit() or s.startswith('+'): raise Invalid('CSV integer '+field) nums[field]=integer(int(s),field,-32768 if field in ('x','y') else 0,32767 if field in ('x','y') else 65535 if 'bin' in field else 255 if field in ('head','site') else 2**31-1) if nums['attempt_order'] in seen: raise Invalid('CSV attempt_order must be globally unique') seen.add(nums['attempt_order']) if row['pass_status'] not in ('PASS','FAIL','UNKNOWN'): raise Invalid('CSV pass_status') lot=row['lot']; wafer=row['wafer']; pid=row['device_id']; x=nums['x']; y=nums['y'] identity=dumps([lot,pid]) if policy['identity']=='part_id' and lot and pid else dumps([lot,wafer,x,y]) if policy['identity']=='wafer_coordinates' and lot and wafer and x not in (None,-32768) and y not in (None,-32768) else None if identity is None or row['pass_status']=='UNKNOWN': issues.append({'code':'UNRESOLVED_CSV_OBSERVATION'}) attempts.append(dict(row,**{k:v for k,v in nums.items() if k!='attempt_order'},file='attempts.csv',file_order=0,offset=nums['attempt_order'],identity=identity)) if not attempts: issues.append({'code':'NO_OBSERVED_ATTEMPTS'}) return attempts,issues def run(payload): try: validate_tree(payload); sources,policy=_sources(payload) attempts=[]; offsets=[]; measurements=[]; issues=[]; hashes=[] for order,name,data,digest in sources: a,o,m,f,complete=parse_binary(data,name,order,policy); attempts+=a; offsets+=o; measurements+=m; issues+=f hashes.append({'filename':name,'order':order,'sha256':digest,'bytes':len(data),'complete':complete}) if 'attempts_csv' in payload: attempts,issues=csv_attempts(payload['attempts_csv'],policy) hashes=[{'filename':'attempts.csv','sha256':sha(payload['attempts_csv'].encode()),'bytes':len(payload['attempts_csv'].encode()),'complete':not issues}] if len(attempts)>MAX_RECORDS or len(offsets)>MAX_RECORDS: raise Invalid('combined record count limit') attempts.sort(key=lambda a:(a['file_order'],a['offset'])) groups=defaultdict(list); locations={} for a in attempts: if a['identity']: loc=(a['wafer'],a['x'],a['y']) if policy['identity']=='part_id' and a['identity'] in locations and locations[a['identity']]!=loc: raise Invalid('part ID location conflict across files') locations[a['identity']]=loc; groups[a['identity']].append(a) devices=[]; transitions=Counter(); bins=Counter() for ident,aa in sorted(groups.items()): first,last=aa[0],aa[-1] devices.append({'identity':ident,'lot':last['lot'],'wafer':last['wafer'],'x':last['x'],'y':last['y'],'device_id':last['device_id'],'attempts':len(aa),'first_status':first['pass_status'],'final_status':last['pass_status'],'first_file':first['file'],'first_offset':first['offset'],'final_file':last['file'],'final_offset':last['offset']}) if len(aa)>1: transitions[(first['pass_status'],last['pass_status'])]+=1 if last['pass_status']!='UNKNOWN': bins[(last['lot'],last['pass_status'],last['hard_bin'],last['soft_bin'])]+=1 complete=not issues; n=len(devices); first=sum(d['first_status']=='PASS' for d in devices); final=sum(d['final_status']=='PASS' for d in devices) summary={'observed_attempts':len(attempts),'observed_resolved_devices':n,'unresolved_attempts':sum(a['identity'] is None for a in attempts),'unknown_attempts':sum(a['pass_status']=='UNKNOWN' for a in attempts),'unique_devices':n if complete else None,'first_pass_count':first if complete else None,'final_pass_count':final if complete else None,'first_pass_rate':first/n if complete and n else None,'final_pass_rate':final/n if complete and n else None,'fail_to_pass':transitions[('FAIL','PASS')] if complete else None,'completeness':'COMPLETE' if complete else 'INCOMPLETE'} tests=defaultdict(list) for m in measurements: if m['value'] is not None: tests[(m['lot'],m['test_number'],m['test_name'],m['units'],m['result_scale'])].append(m['value']) summaries=[dict(zip(('lot','test_number','test_name','units','result_scale'),key),count=len(v),minimum=min(v),maximum=max(v),mean=sum(v)/len(v)) for key,v in sorted(tests.items())] tables={'attempts.csv':attempts,'devices.csv':devices,'retest-transitions.csv':[{'first_status':a,'final_status':b,'devices':n} for (a,b),n in sorted(transitions.items())],'bins.csv':[{'lot':k[0],'status':k[1],'hard_bin':k[2],'soft_bin':k[3],'devices':v} for k,v in sorted(bins.items(),key=lambda kv:dumps(kv[0]))],'measurements.csv':measurements,'test-summaries.csv':summaries,'source-record-offsets.csv':offsets} return {'status':'PASS' if complete else 'UNKNOWN','summary':summary,'tables':tables,'findings':issues,'provenance':{'schema':SCHEMA,'version':VERSION,'sources':hashes,'policy':policy,'synthetic':payload.get('synthetic') is True}} except (Invalid,ValueError,TypeError,KeyError,OverflowError,RecursionError) as e: return {'status':'FAIL','summary':{},'tables':{},'findings':[{'code':'INVALID_INPUT','message':str(e)}],'provenance':{'schema':SCHEMA,'version':VERSION}} def run_json(text): try: return dumps(run(strict(text))) except Invalid as e: return dumps({'status':'FAIL','summary':{},'tables':{},'findings':[{'code':'INVALID_JSON','message':str(e)}],'provenance':{'schema':SCHEMA,'version':VERSION}}) def files_to_payload(files): if not isinstance(files,dict) or not files or len(files)>2: raise Invalid('supported input file set required') for name,entry in files.items(): safe_name(name) if not isinstance(entry,dict) or set(entry)!={'encoding','content'} or not isinstance(entry['content'],str): raise Invalid('file wrapper schema') if 'input.json' in files: if len(files)!=1 or files['input.json']['encoding']!='utf8': raise Invalid('input.json must be the sole UTF8 file') return strict(files['input.json']['content']) if set(files)-{'input.stdf','attempts.csv','settings.json'} or sum(k in files for k in ('input.stdf','attempts.csv'))!=1 : raise Invalid('input.stdf or attempts.csv plus optional settings.json required') if 'settings.json' in files and files['settings.json']['encoding']!='utf8': raise Invalid('settings must be UTF8') result={'schema':SCHEMA,'settings':strict(files['settings.json']['content']) if 'settings.json' in files else {'identity':'wafer_coordinates'}} if 'input.stdf' in files: if files['input.stdf']['encoding']!='base64': raise Invalid('STDF must be base64 bytes') decode64(files['input.stdf']['content']); result['stdf_base64']=files['input.stdf']['content'] else: if files['attempts.csv']['encoding']!='utf8': raise Invalid('CSV must be UTF8') result['attempts_csv']=files['attempts.csv']['content'] return result def csv_bytes(rows): s=io.StringIO(newline=''); fields=sorted({k for row in rows for k in row}) if fields: w=csv.DictWriter(s,fieldnames=fields,lineterminator='\n'); w.writeheader() for row in rows: out={} for k,v in row.items(): if isinstance(v,str) and v.lstrip(' \t\r\n').startswith(('=','+','-','@')): v="'"+v out[k]=v w.writerow(out) return s.getvalue().encode() def map_svg(report, group=None, phase="final"): if phase not in ("first","final"): raise Invalid("map phase") points=[d for d in report['tables']['devices.csv'] if d['x'] is not None and d['y'] is not None and (group is None or (d['lot'],d['wafer'])==group)] rotation=report['provenance']['policy']['rotation_degrees']; points=points[:2000] def rotate(x,y): return {0:(x,y),90:(-y,x),180:(-x,-y),270:(y,-x)}[rotation or 0] coords=[rotate(d['x'],d['y']) for d in points]; extent=max([1]+[abs(z) for p in coords for z in p]); scale=200/extent out=['','+X+Y'] for d,(x,y) in zip(points,coords): color={'PASS':'#166534','FAIL':'#991b1b','UNKNOWN':'#6b7280'}[d[phase+'_status']] label=html.escape(d['identity']+' '+d[phase+'_status']) out.append('%s'%(250+x*scale,250-y*scale,color,label)) out.append((''+phase.title()+' observed state; %s; first 2,000 mapped devices')%('orientation unspecified' if rotation is None else 'rotation '+str(rotation)+' degrees')) return ''.join(out).encode() def offline_page(name,title,status,href,action,content): template = """{title}

{name} · local computation

{title}

{status}

Scope
Supported inputs only
Review
Draft observed output
{action}
{content}

Generated offline from supplied data. No professional sign-off or external actions.

US Tech Automations · 3298 N Glassford Hill Rd Ste 104 PMB 1055, Prescott Valley AZ 86314

""" return template.format(name=html.escape(name),title=html.escape(title),status=html.escape(status),href=html.escape(href,quote=True),action=html.escape(action),content=content) SHARED_CSS = '/* ==========================================================================\n Dated change feeds — aligned to the ustechautomations.com design system.\n Tokens, type scale, radius and accent are lifted from the live main site.\n Take them from assets/WebsiteStyleProvider-*.css, NOT assets/root-*.css: the\n root sheet is the app\'s palette and the website overrides it. Reading the\n wrong one is how these pages ended up with a near-black button where the\n marketing site has a blue one.\n\n Look only. Prices, sample rows, event ids and honesty lines live in the\n HTML and are frozen: never restyle a fact out of existence.\n Each family page sets data-family on . Nothing in this file reads it:\n there is one accent and every family shares it. The attribute is kept for\n the page\'s own scripts and for anyone grepping the built site by family.\n ========================================================================== */\n\n@font-face {\n font-family: "Satoshi";\n src: url("https://ustechautomations.com/fonts/Satoshi-Variable.woff2") format("woff2-variations"),\n url("https://ustechautomations.com/fonts/Satoshi-Variable.woff2") format("woff2");\n font-weight: 300 900;\n font-display: swap;\n font-style: normal;\n}\n\n:root {\n /* type — same stack the main site resolves --font-sans to */\n --sans: "Satoshi", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n --serif: "PT Serif", ui-serif, Georgia, "Times New Roman", serif;\n --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;\n\n /* main-site tokens, light */\n --background: 220 10% 99%;\n --foreground: 220 20% 10%;\n --card: 0 0% 100%;\n --card-foreground: 220 20% 10%;\n --secondary: 220 12% 97%;\n --muted: 220 12% 97%;\n --muted-foreground: 220 10% 42%;\n --border: 220 13% 94%;\n --primary: 207 100% 50%;\n --primary-foreground: 0 0% 100%;\n --primary-hover: 207 100% 44%;\n --accent-blue: 207 100% 50%;\n --accent-emerald: 149 80% 90%;\n --accent-emerald-fg: 161 94% 30%;\n --accent-amber: 48 96% 89%;\n --accent-amber-fg: 26 90% 37%;\n --radius: .5rem;\n --shadow-color: 220 15% 60%;\n\n /* resolved */\n --bg: hsl(var(--background));\n --surface: hsl(var(--card));\n --surface-2: hsl(var(--secondary));\n --fg: hsl(var(--foreground));\n --muted-fg: hsl(var(--muted-foreground));\n --faint: hsl(var(--muted-foreground) / .78);\n --line: hsl(var(--border));\n --line-strong: hsl(var(--border) / 1.6);\n --accent: hsl(var(--accent-blue));\n --shadow-sm: 0 1px 2px hsl(var(--shadow-color) / .08);\n --shadow-md: 0 4px 16px -4px hsl(var(--shadow-color) / .16);\n}\n\n@media (prefers-color-scheme: dark) {\n :root:not([data-theme="light"]) {\n --background: 220 15% 8%;\n --foreground: 220 15% 96%;\n --card: 220 12% 10%;\n --card-foreground: 220 15% 96%;\n --secondary: 220 10% 16%;\n --muted: 220 10% 16%;\n --muted-foreground: 220 8% 62%;\n --border: 220 10% 18%;\n --primary: 207 100% 50%;\n --primary-foreground: 0 0% 100%;\n --primary-hover: 207 100% 44%;\n --accent-blue: 207 100% 60%;\n --accent-emerald: 164 86% 16%;\n --accent-emerald-fg: 158 64% 52%;\n --accent-amber: 22 78% 26%;\n --accent-amber-fg: 45.9 96.7% 64.5%;\n --shadow-color: 220 20% 4%;\n }\n}\n:root[data-theme="dark"] {\n --background: 220 15% 8%;\n --foreground: 220 15% 96%;\n --card: 220 12% 10%;\n --card-foreground: 220 15% 96%;\n --secondary: 220 10% 16%;\n --muted: 220 10% 16%;\n --muted-foreground: 220 8% 62%;\n --border: 220 10% 18%;\n --primary: 207 100% 50%;\n --primary-foreground: 0 0% 100%;\n --primary-hover: 207 100% 44%;\n --accent-blue: 207 100% 60%;\n --accent-emerald: 164 86% 16%;\n --accent-emerald-fg: 158 64% 52%;\n --accent-amber: 22 78% 26%;\n --accent-amber-fg: 45.9 96.7% 64.5%;\n --shadow-color: 220 20% 4%;\n}\n\n*, *::before, *::after { box-sizing: border-box; }\n\nhtml { -webkit-text-size-adjust: 100%; background: hsl(var(--background)); }\n\nbody {\n margin: 0;\n background: var(--bg);\n color: var(--fg);\n font-family: var(--sans);\n font-size: 16px;\n font-weight: 500;\n line-height: 1.65;\n -webkit-font-smoothing: antialiased;\n text-rendering: optimizeLegibility;\n}\n\n.wrap { width: 100%; max-width: 1280px; margin: 0 auto; padding: 0 1.5rem; }\n@media (min-width: 768px) { .wrap { padding: 0 2rem; } }\n\na { color: var(--accent); text-decoration: none; }\na:hover { text-decoration: underline; }\n\n.skip {\n position: absolute; left: -9999px; top: 0;\n background: var(--surface); color: var(--fg);\n padding: .75rem 1rem; border: 1px solid var(--line);\n border-radius: var(--radius); z-index: 100;\n}\n.skip:focus { left: 1rem; top: 1rem; }\n\n/* ---------- masthead: mirrors the main-site sticky header ---------- */\n.masthead {\n position: sticky; top: 0; z-index: 40;\n width: 100%;\n border-bottom: 1px solid var(--line);\n background: hsl(var(--background) / .8);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n}\n.masthead .wrap {\n display: flex; align-items: center; gap: 1.25rem;\n min-height: 64px; flex-wrap: wrap;\n}\n@media (min-width: 1024px) { .masthead .wrap { flex-wrap: nowrap; } }\n.wordmark {\n display: inline-flex; align-items: center; gap: .5rem;\n font-weight: 900; font-size: 1rem; letter-spacing: normal;\n text-transform: uppercase;\n color: var(--fg); text-decoration: none; white-space: nowrap;\n}\n@media (min-width: 640px) { .wordmark { font-size: 1.25rem; letter-spacing: .025em; } }\n.usta-logo { width: 24px; height: 24px; display: block; flex: none; }\n.wordmark:hover { text-decoration: none; opacity: .8; }\n.wordmark span { font-weight: 500; color: var(--muted-fg); }\n\n.mast-nav { display: none; gap: 2rem; margin-left: 2rem; align-items: center; flex: 1; }\n/* main site keeps the links beside the wordmark and the CTA hard right */\n.mast-cta { margin-left: auto; }\n@media (min-width: 1024px) { .mast-nav { display: flex; } }\n.mast-nav a {\n font-size: .875rem; font-weight: 500; color: var(--fg); text-decoration: none;\n}\n.mast-nav a:hover { color: hsl(var(--primary)); text-decoration: none; }\n.mast-cta {\n display: inline-flex; align-items: center;\n background: hsl(var(--primary)); color: hsl(var(--primary-foreground));\n font-size: .875rem; font-weight: 600;\n padding: .5rem 1rem; border-radius: var(--radius);\n text-decoration: none; white-space: nowrap;\n}\n.mast-cta:hover { background: hsl(var(--primary-hover)); text-decoration: none; }\n\n.crumbs {\n width: 100%; margin: 0 0 .75rem; font-size: .8125rem; color: var(--muted-fg);\n}\n.crumbbar { border-bottom: 1px solid var(--line); background: hsl(var(--background)); }\n.crumbbar .crumbs {\n width: auto; margin: 0; padding: .625rem 0;\n white-space: nowrap; overflow-x: auto;\n}\n.crumbs a { color: var(--muted-fg); }\n.crumbs .sep { padding: 0 .5rem; opacity: .5; }\n\n/* ---------- hero ---------- */\n.hero { padding: 3.5rem 0 2rem; }\n.hero h1 {\n font-size: 3rem;\n line-height: 1.15; letter-spacing: -.025em; font-weight: 500;\n margin: 0 0 1rem; max-width: 20ch;\n}\n@media (min-width: 768px) { .hero h1 { font-size: 3.75rem; } }\n@media (min-width: 1024px) { .hero h1 { font-size: 4.5rem; letter-spacing: -.03em; } }\n.eyebrow {\n display: flex; align-items: center; flex-wrap: wrap; gap: .5rem;\n margin: 0 0 1rem;\n font-size: .8125rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;\n color: var(--muted-fg);\n}\n.dot { width: 3px; height: 3px; border-radius: 999px; background: currentColor; opacity: .55; display: inline-block; }\n.lede {\n font-size: clamp(1.0625rem, 1rem + .35vw, 1.25rem);\n line-height: 1.6; color: var(--muted-fg); max-width: 68ch; margin: 0 0 2rem;\n}\n.lede strong { color: var(--fg); font-weight: 600; }\n\n/* ---------- fact rail ---------- */\n.rail {\n display: grid; gap: 1px; margin: 0;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n background: var(--line);\n border: 1px solid var(--line);\n border-radius: var(--radius);\n overflow: hidden;\n}\n.rail > div { background: var(--surface); padding: 1rem 1.25rem; }\n.rail dt {\n font-size: .75rem; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;\n color: var(--muted-fg); margin: 0 0 .375rem;\n}\n.rail dd { margin: 0; font-size: .9375rem; font-weight: 500; }\n.rail dd.price, .amount {\n font-size: 1.25rem; font-weight: 700; letter-spacing: -.02em;\n font-variant-numeric: tabular-nums;\n}\n\n/* ---------- main ---------- */\nmain { padding: 1rem 0 4rem; }\nmain section { margin: 0 0 2.75rem; max-width: 76ch; }\n/* Evidence tables use the full 1280px container the main site\'s sections use,\n while the prose around them keeps its reading measure. */\nmain section:has(.evidence) { max-width: none; }\nmain section:has(.evidence) > p,\nmain section:has(.evidence) > h2,\nmain section:has(.evidence) > h3 { max-width: 76ch; }\n/* Main-site heading character (weight/tracking/leading are its h1-h6 rule).\n Sizes step one rung down its ladder: a feed page\'s h2 is a subsection of a\n record, not a marketing section header, so it takes the site\'s h3 numbers. */\nh2 {\n font-size: 1.875rem; font-weight: 500; letter-spacing: -.025em; line-height: 1.15;\n margin: 0 0 .875rem;\n}\nh3 {\n font-size: 1.5rem; font-weight: 500; letter-spacing: -.025em; line-height: 1.15;\n margin: 0 0 .5rem;\n}\n@media (min-width: 768px) {\n h2 { font-size: 2.25rem; }\n h3 { font-size: 1.875rem; }\n}\np { margin: 0 0 1rem; line-height: 1.625; }\nmain p { color: hsl(var(--foreground) / .86); }\nstrong { font-weight: 650; color: var(--fg); }\n\n/* ---------- pills ---------- */\n.pill {\n display: inline-flex; align-items: center; gap: .375rem;\n font-size: .75rem; font-weight: 650; letter-spacing: .01em;\n padding: .25rem .625rem; border-radius: 999px;\n border: 1px solid transparent; white-space: nowrap;\n}\n.pill-hold {\n background: hsl(var(--accent-amber)); color: hsl(var(--accent-amber-fg));\n border-color: hsl(var(--accent-amber-fg) / .22);\n}\n.pill-ready {\n background: hsl(var(--accent-emerald)); color: hsl(var(--accent-emerald-fg));\n border-color: hsl(var(--accent-emerald-fg) / .22);\n}\n\n/* ---------- honesty panel ---------- */\n.honest {\n border: 1px solid hsl(var(--accent-amber-fg) / .3);\n background: hsl(var(--accent-amber) / .5);\n border-radius: var(--radius);\n padding: 1.125rem 1.25rem;\n}\n.honest p:last-child { margin-bottom: 0; }\n.honest strong { color: hsl(var(--accent-amber-fg)); }\n\n/* The same disclosure, sitting in a hero rather than in the body. Five family\n pages used this class with nothing defined for it, so their "not for sale\n yet" line rendered as ordinary body text while the identical sentence on\n dc-buildout got a panel. One shop, one look. */\n.hero-note {\n border: 1px solid hsl(var(--accent-amber-fg) / .3);\n background: hsl(var(--accent-amber) / .5);\n border-radius: var(--radius);\n padding: 1rem 1.125rem;\n margin: 0 0 1.25rem;\n max-width: 62ch;\n}\n.hero-note strong { color: hsl(var(--accent-amber-fg)); }\n\n/* ---------- spec list ---------- */\n.spec { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: .75rem; }\n.spec li {\n display: grid; gap: .125rem;\n border-left: 2px solid var(--line-strong);\n padding: .125rem 0 .125rem 1rem;\n}\n.spec li strong { font-weight: 650; }\n.sub { color: var(--muted-fg); font-size: .9375rem; }\n\n/* ---------- cards (hub) ---------- */\n.cards { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }\n.hub-groups { display: grid; gap: 2.5rem; }\n.group { max-width: none; }\n\n/* The list of a feed\'s own pages, printed on the feed\'s page. Columns, not one\n long line: twenty-seven states stacked vertically is a wall, and a reader\n scanning for one name reads across a column far faster than down a list.\n break-inside keeps a name from being split over a column boundary. */\n.slice-index { margin-top: 2.75rem; border-top: 1px solid var(--line); padding-top: 1.75rem; }\n.slice-index h2 { margin-bottom: .9rem; }\n.slice-list { columns: 3 190px; column-gap: 1.75rem; list-style: none; padding: 0; margin: 0 0 .9rem; }\n.slice-list li { break-inside: avoid; margin: 0 0 .5rem; }\n.slice-list a { text-decoration: none; }\n.slice-list a:hover { text-decoration: underline; }\n.card {\n display: block; background: var(--surface);\n border: 1px solid var(--line); border-radius: var(--radius);\n padding: 1.25rem; color: inherit; text-decoration: none;\n box-shadow: var(--shadow-sm);\n transition: box-shadow .15s ease, border-color .15s ease, transform .15s ease;\n}\na.card:hover {\n text-decoration: none; border-color: hsl(var(--accent-blue) / .5);\n box-shadow: var(--shadow-md); transform: translateY(-1px);\n}\n.card h3 { margin: 0 0 .375rem; }\n.card .who { color: var(--muted-fg); font-size: .875rem; margin: 0 0 .875rem; }\n.card .meta { display: flex; align-items: center; gap: .625rem; flex-wrap: wrap; font-size: .8125rem; color: var(--muted-fg); }\n\n/* ---------- evidence / sample panels ---------- */\n.evidence {\n border: 1px solid var(--line); border-radius: var(--radius);\n overflow: hidden; background: var(--surface); box-shadow: var(--shadow-sm);\n}\n.evidence-head {\n display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;\n padding: .75rem 1.125rem; background: var(--surface-2);\n border-bottom: 1px solid var(--line);\n font-size: .8125rem; font-weight: 600; color: var(--muted-fg);\n}\n.evidence-body { padding: 1.125rem; }\n.blank { color: var(--muted-fg); font-style: italic; }\n.scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }\n\ntable { width: 100%; border-collapse: collapse; font-size: .875rem; }\nth, td { text-align: left; padding: .625rem .75rem; border-bottom: 1px solid var(--line); }\nth {\n font-size: .6875rem; font-weight: 650; letter-spacing: .05em; text-transform: uppercase;\n color: var(--muted-fg);\n}\ntd.num, .num { font-variant-numeric: tabular-nums; }\ntbody tr:last-child td { border-bottom: 0; }\n\n.stamp, .seal, .fact, .change, .moved {\n font-family: var(--mono); font-size: .8125rem;\n}\nh1 > .seal, h1 > .stamp, h2 > .seal, h2 > .stamp, h3 > .seal, h3 > .stamp {\n display: block; margin-top: .375rem;\n font-weight: 400; letter-spacing: 0; color: var(--muted-fg);\n font-size: .8125rem; line-height: 1.5;\n}\n.seal { color: var(--muted-fg); word-break: break-all; }\n.stamp { color: var(--muted-fg); }\n.note { font-size: .875rem; color: var(--muted-fg); }\n\n.steps { display: grid; gap: 1rem; padding: 0; margin: 0 0 1rem; list-style: none; counter-reset: step; }\n.steps li { display: grid; grid-template-columns: auto 1fr; gap: .875rem; align-items: start; }\n\n/* ---------- contact ---------- */\n.contact {\n border: 1px solid var(--line); border-radius: var(--radius);\n background: var(--surface); padding: 1.75rem; box-shadow: var(--shadow-sm);\n max-width: none;\n}\n.contact h2 { margin-top: 0; }\n.mail {\n display: inline-flex; align-items: center;\n background: hsl(var(--primary)); color: hsl(var(--primary-foreground));\n font-weight: 600; font-size: .9375rem;\n padding: .625rem 1.25rem; border-radius: var(--radius); text-decoration: none;\n}\n.mail:hover { background: hsl(var(--primary-hover)); text-decoration: none; }\n.mail-note { margin: .875rem 0 0; font-size: .8125rem; color: var(--muted-fg); }\n\n/* ---------- footer: mirrors the main-site footer ---------- */\nfooter.site {\n border-top: 1px solid var(--line);\n background: var(--bg);\n padding: 4rem 0 2rem;\n font-size: .875rem;\n}\n@media (min-width: 768px) { footer.site { padding: 5rem 0 2rem; } }\n.foot-grid {\n display: grid; gap: 3rem; margin-bottom: 3rem;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n}\n.foot-brand { grid-column: 1 / -1; }\n@media (min-width: 1024px) { .foot-brand { grid-column: span 2; max-width: 320px; } }\n.foot-brand .wordmark {\n font-size: 1.125rem; font-weight: 600; letter-spacing: -.025em; text-transform: none;\n}\n.foot-brand p { color: var(--muted-fg); margin: .75rem 0 1rem; }\n.foot-col h4 {\n font-size: 1.875rem; font-weight: 500; letter-spacing: -.025em; line-height: 1.15;\n color: var(--fg); margin: 0 0 1rem;\n}\n@media (min-width: 768px) { .foot-col h4 { font-size: 2.25rem; } }\n.foot-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }\n.foot-col a { color: var(--muted-fg); }\n.foot-col a:hover { color: hsl(var(--primary)); }\n.foot-bottom {\n border-top: 1px solid var(--line); padding-top: 1.5rem;\n display: grid; gap: .75rem; color: var(--muted-fg);\n}\nfooter.site p { color: var(--muted-fg); }\nfooter.site .addr { font-size: .8125rem; }\n.foot-honest { max-width: 76ch; }\n\n/* ---------- offer: the pay path ----------\n A page shows btn-buy only when catalog.json carries a checkout that was\n fetched and found working. btn-ghost is the email fallback. Both are the\n same shape so a page does not look broken when the pay link is missing. */\n.hero-cta {\n display: flex; flex-wrap: wrap; align-items: center; gap: .875rem;\n margin: 1.5rem 0 0;\n}\n.btn {\n display: inline-flex; align-items: center; justify-content: center;\n font-weight: 650; font-size: .9375rem; line-height: 1;\n padding: .75rem 1.375rem; border-radius: var(--radius);\n text-decoration: none; border: 1px solid transparent;\n transition: background .15s ease, border-color .15s ease;\n}\n.btn:hover { text-decoration: none; }\n.btn-buy {\n background: hsl(var(--primary)); color: hsl(var(--primary-foreground));\n box-shadow: 0 1px 2px hsl(var(--foreground) / .08);\n}\n.btn-buy:hover { background: hsl(var(--primary-hover)); }\n.btn-ghost {\n background: transparent; color: hsl(var(--primary));\n border-color: hsl(var(--primary) / .35);\n}\n.btn-ghost:hover { background: hsl(var(--primary) / .06); border-color: hsl(var(--primary) / .55); }\n.btn-lg { padding: .9375rem 1.75rem; font-size: 1rem; }\n.btn-note { font-size: .8125rem; color: var(--muted-fg); }\n.contact.buy .buy-price {\n margin: 0 0 1.125rem; font-size: 1.125rem; color: var(--fg);\n}\n.contact.buy .buy-price strong { font-size: 1.5rem; letter-spacing: -.01em; }\n.contact.buy .btn-lg { margin-bottom: .25rem; }\n@media (max-width: 34rem) {\n .hero-cta { flex-direction: column; align-items: flex-start; }\n .btn { width: 100%; }\n}\n' def bundle(payload): report=run(payload) if report['status']=='FAIL': raise Invalid(dumps(report['findings'])) files={'input.json':dumps(payload).encode(),'report.json':dumps(report).encode(),'policy.json':dumps(report['provenance']['policy']).encode(),'completeness.json':dumps({'status':report['status'],'summary':report['summary'],'findings':report['findings']}).encode(),'README.txt':('TestLotReplay '+VERSION+' '+SCHEMA+'\nOffline bounded STDF V4/normalized CSV replay. PASS means supported computation only. UNKNOWN withholds dependent final denominators. Source offsets refer to original uploaded bytes. No manufacturing decisions, equipment maps, process recommendations, or universal vendor compatibility. See policy.json. Re-run: python3 product.py input.json OUTPUT_DIR.\n').encode()} for name,rows in report['tables'].items(): files[name]=csv_bytes(rows) groups=sorted({(d['lot'],d['wafer']) for d in report['tables']['devices.csv']},key=dumps) files['wafer-map.svg']=map_svg(report,groups[0] if groups else None) map_sections=[] for group in groups: ident=sha(dumps(group).encode())[:24] first_name='wafer-'+ident+'-first.svg'; final_name='wafer-'+ident+'-final.svg' files[first_name]=map_svg(report,group,'first');files[final_name]=map_svg(report,group,'final') map_sections.append('

'+html.escape(str(group[0])+' / '+str(group[1]))+'

First observed attempt mapFirst observed results for this lot and wafer
Final observed attempt mapFinal observed results for this lot and wafer
') rows=''.join(''+html.escape(str(d['device_id']))+''+html.escape(d['first_status'])+''+html.escape(d['final_status'])+'' for d in report['tables']['devices.csv'][:2000]) files['styles.css']=SHARED_CSS.encode() files['index.html']=offline_page('TestLotReplay','First and final observed device results',report['status']+' · Bounded replay; dependent totals are withheld when incomplete.','attempts.csv','Download observed attempts',''.join(map_sections)+'

Observed data

First and final outcomes remain separate. No manufacturing acceptance or equipment-ready maps.

'+rows+'
First 2,000 resolved devices; complete rows in devices.csv
DeviceFirstFinal
').encode() for order,name,data,digest in _sources(payload)[0]: files['source-%02d.stdf'%order if order<100 else 'source-'+digest[:16]+'.stdf']=data files['manifest.json']=dumps({'schema':SCHEMA,'version':VERSION,'sha256':{k:sha(v) for k,v in sorted(files.items())}}).encode() b=io.BytesIO() with zipfile.ZipFile(b,'w',zipfile.ZIP_DEFLATED) as z: for name,data in sorted(files.items()): info=zipfile.ZipInfo(name,(1980,1,1,0,0,0)); info.compress_type=zipfile.ZIP_DEFLATED; info.external_attr=0o600<<16; z.writestr(info,data) return b.getvalue() def _fixture(endian='<'): # Original synthetic schema encoder, not a vendor file or input parser. def rec(t,s,b): return struct.pack(endian+'HBB',len(b),t,s)+b def cn(s): b=s.encode(); return bytes([len(b)])+b out=rec(0,10,bytes([2 if endian=='<' else 1,4])) out+=rec(1,10,struct.pack(endian+'IIBBBBH B',0,0,1,32,32,32,0,32)+cn('SYNTHETIC-LOT')) out+=rec(1,80,bytes([1,1,2,1,2])) out+=rec(2,10,struct.pack(endian+'BBI',1,1,0)+cn('W1')) for pid,x,y,status in [('A',-1,0,8),('B',0,1,0),('C',1,0,8),('A',-1,0,0)]: out+=rec(5,10,bytes([1,1])) out+=rec(5,20,struct.pack(endian+'BBBH HHhhI',1,1,status,0,1 if status==0 else 2,1 if status==0 else 2,x,y,1)+cn(pid)) out+=rec(2,20,struct.pack(endian+'BBIIIII',1,1,0,4,1,0,2)+struct.pack(endian+'I',4)+cn('W1')) out+=rec(1,30,struct.pack(endian+'BBI',255,255,4)) return out+rec(1,20,struct.pack(endian+'IB',0,32)) def demo(): return {'schema':SCHEMA,'synthetic':True,'settings':{'identity':'part_id','rotation_degrees':0},'stdf_base64':base64.b64encode(_fixture()).decode()} def atomic(path,data): fd,tmp=tempfile.mkstemp(prefix='.testlot-',dir=path.parent) try: with os.fdopen(fd,'wb') as f: f.write(data); f.flush(); os.fsync(f.fileno()) os.replace(tmp,path) finally: if os.path.exists(tmp): os.unlink(tmp) def main(argv=None): from pathlib import Path argv=sys.argv if argv is None else argv if len(argv)!=3: return 2 try: with open(argv[1],'rb') as f: raw=f.read(MAX_BYTES+1) payload=strict(raw.decode('utf8')); report=run(payload) except (OSError,UnicodeError,Invalid) as e: report={'status':'FAIL','summary':{},'tables':{},'findings':[{'code':'INVALID_INPUT','message':str(e)}],'provenance':{'schema':SCHEMA,'version':VERSION}} try: out=Path(argv[2]); out.mkdir(parents=True,exist_ok=True); out=out.resolve() if report['status']!='FAIL': atomic(out/'result.zip',bundle(payload)) elif (out/'result.zip').exists() or (out/'result.zip').is_symlink(): (out/'result.zip').unlink() atomic(out/'report.json',dumps(report).encode()) except (OSError,Invalid): return 2 return {'PASS':0,'FAIL':2,'UNKNOWN':3}[report['status']] if __name__=='__main__': sys.exit(main())