hubcard-inspector-pro / engine.mjs
Hugdrigo's picture
Upload 12 files
bc4b45e verified
Raw History Blame Contribute Delete
12.2 kB
/**
* 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};
}