diff --git a/rf-diagnostics/app.js b/rf-diagnostics/app.js
new file mode 100644
index 000000000..c546118ee
--- /dev/null
+++ b/rf-diagnostics/app.js
@@ -0,0 +1,48 @@
+'use strict';
+const M=RFDiagnostics,$=id=>document.getElementById(id),ns='http://www.w3.org/2000/svg';
+let generation=0;
+function render(rows){
+ const last=rows.at(-1),kinds=[...new Set(rows.map(r=>M.modes[r.source_kind]))];
+ $('mode').textContent=kinds.join(' + ')||'NO DATA';
+ $('count').textContent=rows.length.toLocaleString();
+ $('rms').textContent=last?.rms_dbfs==null?'—':last.rms_dbfs.toFixed(2);
+ $('rails').textContent=last?.rail_fraction==null?'—':(last.rail_fraction*100).toFixed(3)+'%';
+ $('gaps').textContent=rows.length?rows.reduce((n,r)=>n+BigInt(r.missing_samples),0n).toLocaleString():'—';
+ const reasons=new Map();
+ for(const r of rows)for(const reason of new Set(r.invalid_reasons))reasons.set(reason,(reasons.get(reason)||0)+1);
+ $('reasons').replaceChildren();
+ for(const [reason,count] of reasons){const li=document.createElement('li');li.textContent=reason+' · '+count.toLocaleString();$('reasons').append(li);}
+ $('meta').textContent=last?[
+ 'Source: '+last.source_id,'Origin: '+last.source_kind+' (file/demo replay)',
+ 'Rate: '+(last.sample_rate_hz||'unknown')+' samples/s',
+ 'Centre: '+(last.center_frequency_hz??'unknown')+' Hz (unverified)',
+ 'Run / slot: '+last.run+' / '+last.slot_index,
+ 'Host monotonic ns: '+last.observed_monotonic_ns,
+ 'Absolute sample start: '+(last.sample_start??'unknown'),
+ 'Configuration verified: false','Valid for inference: false',
+ 'TTL: '+last.ttl_ms+' ms; replay freshness unknown'
+ ].join('\n'):'No observations';
+ const tail=rows.slice(-500),values=tail.map(r=>r.rms_dbfs).filter(v=>v!==null);
+ const low=Math.min(...values),high=Math.max(...values),span=high-low||1;
+ $('chart').replaceChildren();
+ let path='';
+ tail.forEach((r,i)=>{
+ if(r.rms_dbfs===null){path+=' ';return;}
+ const x=20+i*960/Math.max(1,tail.length-1),y=220-200*(r.rms_dbfs-low)/span;
+ path+=(i===0||tail[i-1].rms_dbfs===null?'M':'L')+x+','+y+' ';
+ if(r.missing_samples){const dot=document.createElementNS(ns,'circle');dot.setAttribute('cx',x);dot.setAttribute('cy',y);dot.setAttribute('r',4);dot.setAttribute('fill','#ffb66c');$('chart').append(dot);}
+ });
+ const line=document.createElementNS(ns,'path');line.setAttribute('d',path);line.setAttribute('fill','none');line.setAttribute('stroke','#71e7db');line.setAttribute('stroke-width',2);$('chart').prepend(line);
+ $('range').textContent=values.length?'Range '+low.toFixed(2)+' to '+high.toFixed(2)+' dBFS · '+tail.length+' latest records':'No RMS observations';
+ $('chart').setAttribute('aria-label',$('range').textContent);
+}
+$('demo').onclick=()=>{generation++;render(M.demo());$('status').textContent='Synthetic demonstration loaded. No hardware evidence.';};
+$('clear').onclick=()=>{generation++;render([]);$('file').value='';$('status').textContent='Cleared. No observations retained.';};
+$('file').onchange=async()=>{
+ const ticket=++generation,file=$('file').files[0];if(!file)return;
+ try{
+ if(file.size>M.MAX_BYTES)throw Error('File exceeds 10 MiB');
+ const rows=M.parse(await file.text());if(ticket!==generation)return;
+ render(rows);$('status').textContent='Imported '+rows.length+' records. Source labels are unverified claims; this is file replay.';
+ }catch(e){if(ticket!==generation)return;render([]);$('status').textContent='Import rejected: '+e.message;}
+};
diff --git a/rf-diagnostics/index.html b/rf-diagnostics/index.html
new file mode 100644
index 000000000..479103c69
--- /dev/null
+++ b/rf-diagnostics/index.html
@@ -0,0 +1,12 @@
+
+
+
+RuView RF Diagnostics
+
+
+Import bridge diagnostics or explore a clearly labelled synthetic example.
+
+Latest RMS
—relative dBFS, not calibrated RF powerLatest rails
—samples at numeric limitsReported missing
—sum of per record missing samplesObservations
0all v1 observations invalid
+Relative RMS history
Record order, last 500 observations. Missing metrics break the line. Orange dots flag missing samples.
No observations
+Source metadata
No observations
+
diff --git a/rf-diagnostics/model.js b/rf-diagnostics/model.js
new file mode 100644
index 000000000..f27e4fa29
--- /dev/null
+++ b/rf-diagnostics/model.js
@@ -0,0 +1,46 @@
+(function(root){
+'use strict';
+const MAX_BYTES=10*1024*1024, MAX_RECORDS=10000;
+const fields='schema source_kind source_id observed_monotonic_ns run slot_index sample_start sample_rate_hz center_frequency_hz config_verified valid invalid_reasons sample_count rms_dbfs rail_fraction missing_samples event_count overrun_slices ttl_ms'.split(' ');
+const modes={synthetic:'SYNTHETIC',live:'LIVE UNVERIFIED',captured_replay:'CAPTURED REPLAY'};
+function validate(r){
+ const fail=(s)=>{throw Error(s)};
+ if(!r||Array.isArray(r)||typeof r!=='object')fail('Expected an object');
+ for(const k of Object.keys(r))if(!fields.includes(k))fail('Unsupported field: '+k);
+ for(const k of fields)if(!Object.hasOwn(r,k))fail('Missing field: '+k);
+ if(r.schema!=='ruview.rf_diagnostics.v1')fail('Unsupported schema');
+ if(!Object.hasOwn(modes,r.source_kind)||r.source_id!=='w1700k')fail('Unsupported source');
+ for(const k of ['observed_monotonic_ns','run','slot_index','sample_rate_hz','sample_count','missing_samples','event_count','overrun_slices','ttl_ms'])
+ if(!Number.isSafeInteger(r[k])||r[k]<0)fail('Invalid '+k);
+ if(r.ttl_ms!==1000)fail('Invalid rate or TTL');
+ for(const k of ['sample_start','center_frequency_hz'])
+ if(r[k]!==null&&(!Number.isSafeInteger(r[k])||r[k]<0))fail('Invalid '+k);
+ if(r.rms_dbfs!==null&&(typeof r.rms_dbfs!=='number'||!Number.isFinite(r.rms_dbfs)))fail('Invalid RMS');
+ if(r.rail_fraction!==null&&(typeof r.rail_fraction!=='number'||!Number.isFinite(r.rail_fraction)||r.rail_fraction<0||r.rail_fraction>1))fail('Invalid rail fraction');
+ if(r.config_verified!==false||r.valid!==false)fail('v1 must remain unverified and invalid');
+ if(!Array.isArray(r.invalid_reasons)||r.invalid_reasons.length>32||!r.invalid_reasons.includes('configuration_unverified')||r.invalid_reasons.some(x=>typeof x!=='string'||!/^[a-z_]{1,64}$/.test(x)))fail('Invalid reasons');
+ if(![0,40000000,80000000,160000000,320000000].includes(r.sample_rate_hz))fail('Invalid sample rate');
+ if(r.sample_start!==null)fail('Absolute sample start is unavailable in v1');
+ if(r.center_frequency_hz!==null&&(r.center_frequency_hz<2000000000||r.center_frequency_hz>8000000000))fail('Invalid centre frequency');
+ if(![0,2048].includes(r.sample_count)||r.missing_samples%2048!==0)fail('Invalid sample count');
+ if(r.rms_dbfs!==null&&(r.rms_dbfs< -200||r.rms_dbfs>16||r.rail_fraction===null))fail('Invalid RMS or missing rail fraction');
+ const reasons=['configuration_unverified','lost','event','overrun','stale','tag_mismatch','run_change'];
+ if(new Set(r.invalid_reasons).size!==r.invalid_reasons.length||r.invalid_reasons.length>7||r.invalid_reasons.some(x=>!reasons.includes(x)))fail('Unsupported invalid reason');
+ return r;
+}
+function parse(text){
+ if(typeof text!=='string'||new TextEncoder().encode(text).length>MAX_BYTES)throw Error('File exceeds 10 MiB');
+ const lines=text.split(/\r?\n/).filter(x=>x.trim());
+ if(!lines.length||lines.length>MAX_RECORDS)throw Error('Expected 1 to 10,000 records');
+ return lines.map((s,i)=>{try{return validate(JSON.parse(s))}catch(e){throw Error('Record '+(i+1)+': '+e.message)}});
+}
+function demo(){return Array.from({length:96},(_,i)=>({
+ schema:'ruview.rf_diagnostics.v1',source_kind:'synthetic',source_id:'w1700k',
+ observed_monotonic_ns:i*100000000,run:1,slot_index:i,sample_start:null,
+ sample_rate_hz:40000000,center_frequency_hz:2437000000,config_verified:false,valid:false,
+ invalid_reasons:i%23===0?['configuration_unverified','lost']:['configuration_unverified'],
+ sample_count:2048,rms_dbfs:-34+Math.sin(i/8)*6,rail_fraction:i%23===0?0.03:0,
+ missing_samples:i%23===0?2048:0,event_count:0,overrun_slices:0,ttl_ms:1000}));}
+const api={MAX_BYTES,MAX_RECORDS,modes,validate,parse,demo};
+if(typeof module==='object')module.exports=api;else root.RFDiagnostics=api;
+})(globalThis);
diff --git a/rf-diagnostics/style.css b/rf-diagnostics/style.css
new file mode 100644
index 000000000..408dce670
--- /dev/null
+++ b/rf-diagnostics/style.css
@@ -0,0 +1 @@
+:root{color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui,sans-serif;background:#080f17;color:#d9e8ee}*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 85% 0,#113642,transparent 50%)}main{max-width:1200px;margin:auto;padding:48px 28px}header{position:relative}.eyebrow{letter-spacing:.2em;color:#62d9da;font-size:12px;font-weight:700}h1{font-size:clamp(38px,7vw,76px);letter-spacing:-.055em;margin:18px 0}.intro{font-size:20px;color:#a4bcc6}.badge{display:inline-block;padding:8px 14px;border:1px solid #b89851;border-radius:32px;color:#f5d18c;font-size:12px;font-weight:bold}.controls{margin-top:32px;display:flex;gap:12px;flex-wrap:wrap;align-items:center}.controls p{flex-basis:100%;font-size:13px;color:#a4bcc6}button,.button{font:inherit;font-size:14px;border:1px solid #38616f;background:#152b37;color:#e7faff;border-radius:10px;padding:13px 18px;cursor:pointer}.button{background:#77e9dd;color:#082b2c;font-weight:700;position:relative;overflow:hidden}.button input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}button:hover,.button:hover{filter:brightness(1.2)}button:focus-visible,.button:focus-within{outline:3px solid #fff;outline-offset:3px}aside{border-left:3px solid #e5b85e;padding:16px 20px;background:#30291c;font-size:14px;line-height:1.6;color:#f5ddab}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:24px 0}.metrics article,.panel{border:1px solid #25414d;background:#0d1c26;border-radius:14px;padding:22px}.metrics p{font-size:13px;color:#9cb6c1}.metrics strong{font-size:30px;color:#79eddf;display:block;margin:16px 0}.metrics small{color:#9cb6c1;font-size:11px}.panel{margin-bottom:18px}h2{font-size:18px;margin-top:0}.panel p,footer{color:#9cb6c1;font-size:13px;line-height:1.6}svg{width:100%;height:auto;display:block;background:linear-gradient(#132c37,#0d1c26);border-radius:8px}.split{display:grid;grid-template-columns:1fr 1fr;gap:18px}pre{white-space:pre-wrap;word-break:break-word;color:#b7d1da;line-height:1.7;font-size:12px}li{margin-bottom:10px;overflow-wrap:anywhere}#status{font-size:14px;min-height:22px}footer{margin:24px 0} @media(max-width:700px){main{padding:28px 16px}.metrics{grid-template-columns:1fr 1fr}.split{grid-template-columns:1fr;gap:0}.metrics article{padding:14px}.metrics strong{font-size:24px}}