/** * HubCard Inspector Pro v2.0 * Deterministic signals about documentation presence, never certification. * This module has no DOM access and runs in browsers and Node for testing. */ export const VERSION='2.0.0'; const colors=['red','yellow','green','blue','indigo','purple','pink','gray']; export const HF_SPACE_COLORS=colors; // Rules are based on documentation practices, not claims of scientific truth. // Each group weights sum to 100 and the presence check is intentionally conservative. export const RULES={ model:[ ['license','License metadata',8,'meta',['license'],'Declare a clear redistribution license in YAML metadata.','identity'], ['language','Language metadata',5,'meta',['language'],'Identify supported languages.','identity'], ['task','Task or library metadata',7,'meta',['pipeline_tag','library_name'],'Describe the ML task or library.','identity'], ['overview','Model description',10,'section',['model details','model description','overview','about the model','introduction'],'Describe the architecture, author and purpose.','transparency'], ['intended','Intended uses',10,'section',['intended use','intended uses','direct use','use cases','applications'],'Document suitable uses.','transparency'], ['limitations','Limitations and out-of-scope',12,'section',['limitations','out-of-scope','out of scope','risks and limitations','limitations and bias'],'Document misuse risks and limitations.','risks'], ['data','Training data provenance',10,'section',['training data','datasets','data sources','dataset','data provenance'],'Identify provenance and conditions of training data.','reproducibility'], ['method','Training methodology',10,'section',['training procedure','training details','methodology','training','implementation details','architecture'],'Document how the model was made.','reproducibility'], ['evaluation','Evaluation and test design',12,'section',['evaluation','results','benchmark','metrics','testing data'],'Report testing data, metrics and known gaps.','reproducibility'], ['usage','Usage or inference example',8,'section',['how to use','usage','examples','usage example','getting started','inference'],'Provide a reproducible usage example.','utility'], ['ethics','Ethics and bias',5,'section',['ethical considerations','responsible use','bias','privacy','societal impact'],'Document foreseeable harm and ethical limits.','risks'], ['version','Version or maintenance',3,'section',['versions','version','changelog','provenance','maintenance','citation'],'Explain maintenance and provenance.','identity'] ], dataset:[ ['license','License metadata',10,'meta',['license'],'Document permission to redistribute the dataset.','identity'], ['language','Language metadata',5,'meta',['language'],'State languages where relevant.','identity'], ['task','Task metadata',5,'meta',['task_categories','task_ids'],'Identify intended data tasks.','identity'], ['description','Dataset description',10,'section',['dataset description','description','overview','about the dataset'],'Explain the data content and collection scope.','transparency'], ['source','Data source or provenance',12,'section',['source data','source','provenance','dataset creation','data sources'],'Document where data originated.','reproducibility'], ['processing','Curation methodology',10,'section',['data processing','methodology','curation','collection process','preprocessing'],'Describe sampling, cleanup and labeling.','reproducibility'], ['schema','Features and schema',8,'section',['features','schema','fields','data fields','columns'],'Document each field and type.','utility'], ['splits','Dataset size and splits',8,'section',['dataset splits','splits','data splits','data instances','size'],'Specify sample counts and partitions.','utility'], ['uses','Intended uses',10,'section',['intended use','intended uses','use cases','supported tasks'],'Explain intended and excluded use.','transparency'], ['limits','Limitations and bias',12,'section',['limitations','bias','risks','known limitations','dataset limitations'],'State biases, missingness and failure modes.','risks'], ['examples','Representative examples',5,'section',['examples','sample records','sample','data instances'],'Show representative records without leaking PII.','utility'], ['privacy','Privacy and sensitive content',5,'section',['privacy','sensitive data','personal data','pii','consent'],'Document privacy risks and permissions.','risks'] ], space:[ ['license','License metadata',8,'meta',['license'],'Declare a software license.','identity'], ['title','Title and description',8,'meta',['title','short_description'],'Add readable metadata for discovery.','identity'], ['purpose','Problem and purpose',12,'section',['purpose','problem','what it does','overview','about'],'Explain the actual problem solved.','transparency'], ['usage','How to use',12,'section',['how to use','usage','instructions','getting started'],'Provide simple step-by-step instructions.','utility'], ['example','Example or demo',8,'section',['examples','example','demo','try it'],'Provide a real end-to-end walkthrough.','utility'], ['architecture','Technology and architecture',10,'section',['architecture','technology','technologies','implementation','how it works'],'Explain the main components and runtime.','reproducibility'], ['limits','Limitations',12,'section',['limitations','known issues','caveats','constraints'],'Describe what the application cannot do.','risks'], ['privacy','Privacy and security',10,'section',['privacy','security','data handling'],'State whether user inputs leave the browser.','risks'], ['sources','Attribution and data sources',8,'section',['source attribution','sources','data sources','attribution','credits','references'],'Credit upstream code, models and datasets.','reproducibility'], ['maintenance','Maintenance and version',6,'section',['maintenance','version','status','roadmap','changelog'],'Describe support, status and versioning.','identity'], ['responsible','Responsible-use guidance',6,'section',['responsible use','responsible ai','ethics','risks','safety'],'Explain consequential-use risks.','risks'] ] }; function stripQuotes(v){return String(v??'').trim().replace(/^(['"])(.*)\1$/,'$2');} export function parseFrontmatter(readme){ const src=String(readme??'').replace(/^\uFEFF/,''); const match=/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\s*\r?\n|$)/.exec(src); if(!match) return {}; const out={};let active=null; for(const raw of match[1].split(/\r?\n/)){ if(/^\s*#/.test(raw)||!raw.trim())continue; const key=/^([a-zA-Z_][\w-]*)\s*:\s*(.*)$/.exec(raw); if(key){ active=key[1];const val=key[2].trim(); if(!val){out[active]=[];continue;} if(/^\[.*\]$/.test(val))out[active]=val.slice(1,-1).split(',').map(stripQuotes).filter(Boolean); else out[active]=stripQuotes(val.split(/\s+#/)[0]); }else if(active && /^\s*-\s+/.test(raw)){ if(!Array.isArray(out[active]))out[active]=[]; out[active].push(stripQuotes(raw.trim().slice(2))); } } return out; } export function headings(readme){ return String(readme??'').split(/\r?\n/).map((line,index)=>({line:line.trim(),number:index+1})) .filter(item=>/^#{1,6}\s+\S/.test(item.line)).map(item=>({...item,heading:item.line.replace(/^#{1,6}\s+/,'').replace(/\s+#+$/,'').trim()})); } function normalized(v){return String(v).normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,' ').trim();} function headingHit(hs,patterns){ const ps=patterns.map(normalized); return hs.find(x=>ps.some(p=>normalized(x.heading)===p || normalized(x.heading).startsWith(p+' ') || normalized(x.heading).endsWith(' '+p))); } function nonempty(v){return v!==undefined && v!==null && (!Array.isArray(v) || v.length>0) && String(v).trim()!=='';} export function auditCard(type,readme,externalMetadata={},repoName='Untitled card'){ if(!RULES[type])throw new Error('Unsupported repository type'); const m={...externalMetadata,...parseFrontmatter(readme)}; const hs=headings(readme); const checks=RULES[type].map(([id,label,points,kind,targets,remedy,category])=>{ let evidence='',pass=false; if(kind==='meta'){ const key=targets.find(k=>nonempty(m[k])); if(key){pass=true;evidence=`Metadata field: ${key}`;} }else{ const item=headingHit(hs,targets); if(item){pass=true;evidence=`Heading at line ${item.number}: ${item.heading}`;} } return {id,label,points,earned:pass?points:0,pass,category,remedy,evidence,headingSuggestion:targets[0]}; }); const score=checks.reduce((s,c)=>s+c.earned,0); const total=checks.reduce((s,c)=>s+c.points,0); const missing=checks.filter(c=>!c.pass); return {type,repoName,score,possible:total,checks,missing,passed:checks.length-missing.length,totalChecks:checks.length,metadata:m, disclaimer:'Heuristic documentation coverage only. Not scientific validation, compliance, fairness or safety certification.'}; } export function buildRemediation(result){ const lines=['## Suggested documentation sections','', '> Replace placeholders with verified facts. Do not publish unsupported claims.','']; const seen=new Set(); for(const c of result.missing){ if(['license','language','task','title'].includes(c.id)){ lines.push(`- **${c.label}:** ${c.remedy} Add truthful YAML metadata.`,` `);continue; } const heading=c.headingSuggestion.replace(/\b\w/g,k=>k.toUpperCase()); if(seen.has(heading))continue; seen.add(heading); lines.push(`### ${heading}`,'',`[To be completed with verified information: ${c.remedy}]`,''); } return lines.join('\n'); } export function buildReport(results){ const lines=['# HubCard Inspector Pro - Documentation Audit','',`Report created: ${new Date().toISOString()}`,'', '**Interpretation:** Scores are heuristic presence checks, not content verification or safety certifications.','']; for(const r of results){ lines.push(`## ${r.repoName} (${r.type})`,'',`Documentation coverage: **${r.score}/${r.possible}**. Signals detected: ${r.passed}/${r.totalChecks}.`,'','| Signal | Points | Evidence |','|---|---:|---|'); for(const c of r.checks)lines.push(`| ${c.label.replace(/\|/g,'/')} | ${c.earned}/${c.points} | ${(c.evidence||'Not detected').replace(/\|/g,'/')} |`); lines.push('','### Improvement priorities'); if(!r.missing.length)lines.push('- No missing signals under this rubric. Verify substantive quality separately.'); for(const c of r.missing)lines.push(`- **${c.label} (${c.points} pts):** ${c.remedy}`); lines.push(''); } return lines.join('\n'); } export function csvReport(results){ const q=v=>'"'+String(v??'').replace(/"/g,'""')+'"'; return [['repository','type','score','signal','points_earned','points_possible','evidence','recommendation'].map(q).join(',')] .concat(results.flatMap(r=>r.checks.map(c=>[r.repoName,r.type,r.score,c.label,c.earned,c.points,c.evidence,c.pass?'':c.remedy].map(q).join(',')))).join('\n'); } export function parseRepoLine(raw,defaultType='model'){ let s=String(raw).trim(),type=defaultType; const prefix=/^(model|dataset|space)\s*:\s*/i.exec(s); if(prefix){type=prefix[1].toLowerCase();s=s.slice(prefix[0].length).trim();} s=s.replace(/^https?:\/\/(?:www\.)?huggingface\.co\//i,''); if(s.startsWith('datasets/')){type='dataset';s=s.slice(9);} else if(s.startsWith('spaces/')){type='space';s=s.slice(7);} s=s.split(/[?#]/)[0].replace(/\/(?:tree|blob|resolve|raw)\/.*$/,'').replace(/\/$/,''); if(!/^[\w.-]+\/[\w.-]+$/.test(s))throw new Error(`Invalid repository: ${raw}`); return {type,id:s}; } export function validateSpaceReadme(text){ const m=parseFrontmatter(text),errors=[]; if(m.sdk!=='static')errors.push('sdk must be static'); for(const k of ['colorFrom','colorTo'])if(m[k]&&!colors.includes(m[k]))errors.push(`${k} is not permitted`); if(!m.short_description||m.short_description.length>60)errors.push('short_description must be 1–60 characters'); if(!m.app_file)errors.push('app_file is missing'); return {valid:errors.length===0,errors,metadata:m}; }