Independent education · Sources and limitations documented
Portfolio / Portfolio records

Portfolio record

Source snapshot · 5 November 2026

tools/site/build-site.mjs

import fs from 'node:fs/promises';
import path from 'node:path';
import crypto from 'node:crypto';
import {execFileSync} from 'node:child_process';
import { pathToFileURL } from 'node:url';
const root=path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/,'$1'));
const markedPath=process.argv[2]||process.env.MARKED_MODULE;
const {Marked,Renderer}=markedPath?await import(pathToFileURL(markedPath).href):await import('./vendor/marked.esm.js');
const out=path.join(root,'dist'),src=path.join(root,'sources');
const pins=JSON.parse(await fs.readFile(path.join(root,'source-pins.json'),'utf8'));
const projects=JSON.parse(await fs.readFile(path.join(root,'projects.json'),'utf8'));
const config=JSON.parse(await fs.readFile(path.join(root,'site-config.json'),'utf8'));
const walk=async(d)=>{let files=[];for(const e of await fs.readdir(d,{withFileTypes:true})){const p=path.join(d,e.name);files.push(...(e.isDirectory()?await walk(p):[p]));}return files;};
const esc=s=>String(s).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('"','&quot;');
const pos=s=>s.replaceAll('\\','/');
const slug=s=>s.toLowerCase().replace(/<[^>]*>/g,'').replace(/&[a-z]+;/g,'').replace(/[^\p{L}\p{N}_\-\s]/gu,'').replace(/\s+/g,'-');
const rel=(from,to)=>pos(path.posix.relative(path.posix.dirname(from),to))||path.posix.basename(to);
const save=async(p,c)=>{await fs.mkdir(path.dirname(path.join(out,p)),{recursive:true});await fs.writeFile(path.join(out,p),c);};
await fs.mkdir(out,{recursive:true});
const cm=path.join(src,'CM-2026');
let home=await fs.readFile(path.join(cm,'index.html'),'utf8');
const baseHeader=home.match(/<header class="site-header">[\s\S]*?<\/header>/)[0];
const baseFooter=home.match(/<footer class="site-footer">[\s\S]*?<\/footer>/)[0];
const filesByRepo=new Map();
const documents=[];const map=new Map();const sourceManifest=[];
for(const pin of pins){const base=path.join(src,pin.name);const paths=(await walk(base)).map(p=>pos(path.relative(base,p))).sort();filesByRepo.set(pin.name,new Set(paths));for(const file of paths){const buf=await fs.readFile(path.join(base,file));sourceManifest.push({repo:pin.name,path:file,commit:pin.sha,bytes:buf.length,sha256:crypto.createHash('sha256').update(buf).digest('hex')});await save('downloads/'+pin.name+'/'+file,buf);if(pin.name==='MCAT'&&!(/\.md$/i.test(file)||file==='LICENSE'))continue;if(pin.name==='CM-2026'&&(file.startsWith('assets/')||file.startsWith('.github/')||file==='index.html'))continue;if(file.startsWith('.'))continue;const project=projects.find(p=>p.repo===pin.name);const route=pin.name==='CM-2026'?'pages/'+file.replace(/\.md$/i,'').replaceAll('/','--')+'.html':'projects/'+project.slug+'/'+file.replace(/\.md$/i,'').replaceAll('/','--')+'.html';const text=/\.(pdf|png|jpe?g|gif|webp|zip)$/i.test(file)?'':buf.toString('utf8');const title=(text.match(/^#\s+(.+)$/m)?.[1]||file).replace(/[*_]/g,'');const doc={repo:pin.name,file,route,text,title,project,pin,bytes:buf.length};documents.push(doc);map.set(pin.name+'/'+file,doc);}}
const repoIndex=n=>n==='CM-2026'?'library.html':'projects/'+projects.find(p=>p.repo===n).slug+'/index.html';
const sourceURL=(n,f)=>'https://github.com/collingeorge/'+n+'/blob/'+pins.find(p=>p.name===n).sha+'/'+f.split('/').map(encodeURIComponent).join('/');
function resolveLink(href,repo,file,from,asset=false){if(!href)return href;let value=href.replaceAll('&amp;','&');if(value.startsWith('#'))return value;if(/^(javascript|vbscript|data):/i.test(value))return '#';if(value.startsWith('https://collingeorge.github.io/MCAT'))return rel(from,'study/mcat/index.html')+(value.split('#')[1]?'#'+value.split('#')[1]:'');if(value.startsWith('https://collingeorge.github.io/CM-2026/')){const u=new URL(value);return rel(from,'index.html')+u.hash;}if(/^https:\/\/github\.com\/collingeorge\/[^/]+\/(?!blob\/|tree\/)[^#]/.test(value))return href;let targetRepo=repo,targetFile;let hash='';const gh=value.match(/^https:\/\/(?:github\.com\/collingeorge|raw\.githubusercontent\.com\/collingeorge)\/([^/#?]+)(?:\/(?:blob|tree)\/[^/]+|\/[^/]+)?\/?(.*)$/);if(gh&&filesByRepo.has(gh[1])){targetRepo=gh[1];targetFile=decodeURIComponent(gh[2].split(/[?#]/)[0]||'README.md');hash=value.includes('#')?'#'+value.split('#')[1]:'';}else if(/^[a-z][a-z0-9+.-]*:/i.test(value)||value.startsWith('//'))return href;else{hash=value.includes('#')?'#'+value.split('#')[1]:'';const plain=decodeURIComponent(value.split(/[?#]/)[0]);targetFile=path.posix.normalize(path.posix.join(path.posix.dirname(file),plain));}if(targetFile.endsWith('/'))targetFile+='README.md';if(targetRepo==='Smart-NOA-Controller'&&/^simulator\/(?!tests\/).+\.(html|css|js|json|svg)$/.test(targetFile)&&filesByRepo.get(targetRepo)?.has(targetFile))return rel(from,targetFile)+hash;if(asset&&filesByRepo.get(targetRepo)?.has(targetFile))return rel(from,'downloads/'+targetRepo+'/'+targetFile)+hash;const target=map.get(targetRepo+'/'+targetFile);if(target)return rel(from,target.route)+hash;if(filesByRepo.get(targetRepo)?.has(targetFile))return rel(from,'downloads/'+targetRepo+'/'+targetFile)+hash;if(targetFile==='README.md')return rel(from,repoIndex(targetRepo))+hash;return sourceURL(targetRepo,targetFile)+hash;}
function rewrite(content,repo,file,route){return content.replace(/\b(href|src)="([^"]*)"/g,(m,attr,url)=>attr+'="'+esc(resolveLink(url,repo,file,route,attr==='src'))+'"');}
function chrome(route){let head=baseHeader.replaceAll('href="#','href="'+rel(route,'index.html')+'#').replace('href="https://collingeorge.github.io/MCAT/"','href="'+rel(route,'study/mcat/index.html')+'"');head=head.replace(/<a href="[^"]*#library">Project library<\/a>/,'<a href="'+rel(route,'library.html')+'">Reading library</a>');let foot=rewrite(baseFooter,'CM-2026','index.html',route).replaceAll('href="#','href="'+rel(route,'index.html')+'#');return {head,foot};}
function page(route,title,body,extra=''){const c=chrome(route);return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="'+esc(title)+' — independent clinical education and research portfolio."><meta name="theme-color" content="#18374b"><title>'+esc(title)+' | Collin B. George</title><link rel="canonical" href="'+config.siteUrl.replace(/\/$/,'')+'/'+route+'"><link rel="icon" href="'+rel(route,'assets/favicon.svg')+'"><link rel="stylesheet" href="'+rel(route,'assets/portfolio.css')+'?v=full-html-1"><link rel="stylesheet" href="'+rel(route,'assets/reading.css')+'?v=full-html-1"><script src="'+rel(route,'assets/portfolio.js')+'" defer></script><script src="'+rel(route,'assets/reading.js')+'?v=full-html-2" defer></script>'+extra+'</head><body><a class="skip-link" href="#main">Skip to content</a><div class="status-bar"><div class="container">Independent education · Sources and limitations documented</div></div>'+c.head+'<main id="main">'+body+'</main>'+c.foot+'</body></html>';}
const search=[];let diagrams=0;
for(const d of documents){const headings=[];const seen=new Map();const renderer=new Renderer();renderer.heading=function({tokens,depth}){const text=this.parser.parseInline(tokens);let id=slug(text);const count=seen.get(id)||0;seen.set(id,count+1);if(count)id+='-'+count;headings.push({level:depth,id,text:text.replace(/<[^>]*>/g,'')});return '<h'+depth+' id="'+esc(id)+'">'+text+'</h'+depth+'>\n';};renderer.code=function({text,lang}){if(lang==='mermaid'){diagrams++;return '<figure class="decision-figure"><div class="diagram-scroll" tabindex="0" aria-label="Scrollable educational decision tree"><pre class="mermaid">'+esc(text)+'</pre></div><details><summary>Read the decision-tree source</summary><pre><code>'+esc(text)+'</code></pre></details><figcaption>Synthetic educational reasoning. Read the cited source and applicability limits.</figcaption></figure>'; }return '<pre><code'+(lang?' class="language-'+esc(lang)+'"':'')+'>'+esc(text)+'</code></pre>';};
const md=new Marked({renderer,gfm:true});let content;
if(/\.md$/i.test(d.file)||d.file==='LICENSE'||d.file==='CHANGELOG'){const source=d.text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/,'');content=md.parse(source);}else if(/\.(pdf|png|jpe?g|gif|webp)$/i.test(d.file)){const download=rel(d.route,'downloads/'+d.repo+'/'+d.file);content='<h1>'+esc(d.title)+'</h1><p>This is a retained source export or figure. Its original file is preserved. Read the project’s current source and revision notices for context.</p><p><a class="button button-primary" href="'+download+'">Open original file ↗</a></p>'+(/\.(png|jpe?g|gif|webp)$/i.test(d.file)?'<img src="'+download+'" alt="'+esc(d.title)+'">':'');}else if(/\.html$/i.test(d.file)){content='<h1>'+esc(d.title)+'</h1><p>Retained dated HTML export. Current reading pages are linked from the project index.</p><a href="'+rel(d.route,'downloads/'+d.repo+'/'+d.file)+'">Open retained HTML export ↗</a>';}else content='<h1>'+esc(d.title)+'</h1><pre class="source-record"><code>'+esc(d.text)+'</code></pre>';
content=content.replace(/<a id="([^"]+)"><\/a>/g,(m,id)=>headings.some(h=>h.id===id)?'':m);content=rewrite(content,d.repo,d.file,d.route);
// Remove active source HTML; input snapshots are fixed and public, but they remain data.
content=execFileSync(process.env.PYTHON||'python',[path.join(root,'sanitize-html.py')],{input:content,encoding:'utf8',maxBuffer:16*1024*1024});
content=content.replace(/<table>/g,'<div class="table-scroll" tabindex="0" aria-label="Scrollable data table"><table>').replace(/<\/table>/g,'</table></div>');
const toc=headings.filter(h=>h.level<=3).map(h=>'<li class="toc-level-'+h.level+'"><a href="#'+esc(h.id)+'">'+esc(h.text)+'</a></li>').join('');
const related=documents.filter(x=>x.repo===d.repo&&['README.md','START_HERE.md','AUTHORSHIP_AND_REVIEW.md','PUBLICATION_REVIEW.md','EVIDENCE_CLASSIFICATION.md','REFERENCE_AUDIT.md','EVIDENCE_AUDIT.md'].includes(x.file)).map(x=>'<li><a href="'+rel(d.route,x.route)+'">'+esc(x.file.replace('.md','').replaceAll('_',' ').toLowerCase())+'</a></li>').join('');
const label=d.project?.title||'Portfolio records';const body='<div class="container reading-breadcrumb"><a href="'+rel(d.route,'index.html')+'">Portfolio</a><span> / </span><a href="'+rel(d.route,repoIndex(d.repo))+'">'+esc(label)+'</a></div><div class="container reading-layout"><aside class="reading-sidebar"><a class="text-link" href="'+rel(d.route,repoIndex(d.repo))+'">← All project documents</a><details class="toc" open><summary>On this page</summary><ol>'+toc+'</ol></details><div class="related-docs"><p class="eyebrow">Project context</p><ul>'+related+'</ul></div></aside><div class="reading-main"><div class="document-meta"><p class="eyebrow">'+esc(d.project?.kind||'Portfolio record')+'</p><span>Source snapshot · 5 November 2026</span></div><div class="document-actions"><a href="'+rel(d.route,'downloads/'+d.repo+'/'+d.file)+'" download>Download source</a><a href="'+sourceURL(d.repo,d.file)+'">View source revision ↗</a><button type="button" class="print-page">Print / save PDF</button></div><article class="prose">'+content+'</article><div class="document-provenance"><strong>Source and scope</strong><p>Published from '+esc(d.repo+'/'+d.file)+' at revision <code>'+d.pin.sha.slice(0,12)+'</code>. Full source text is retained; HTML presentation does not establish independent clinical review. '+esc(d.project?.boundary||'See authorship, provenance and review records.')+'</p></div></div></div>';
const hasDiagram=content.includes('class="mermaid"');await save(d.route,page(d.route,d.title,body,hasDiagram?'<script src="'+rel(d.route,'assets/vendor/mermaid.min.js')+'" defer></script>':''));search.push({title:d.title,project:label,kind:d.project?.kind||'Portfolio record',url:d.route,text:d.text.replace(/<[^>]*>/g,' ').replace(/\s+/g,' ').trim()});}
for(const p of projects){const route=repoIndex(p.repo);const docs=documents.filter(d=>d.repo===p.repo);const entry=map.get(p.repo+'/'+p.entry)||map.get(p.repo+'/README.md');const primary=['README.md','START_HERE.md',p.entry,'APPLIED_CASES.md','ANESTHESIA_OPTIONS_AND_CASES.md',p.evidence];const ordered=[...docs].sort((a,b)=>{const ai=primary.indexOf(a.file),bi=primary.indexOf(b.file);return(ai<0?999:ai)-(bi<0?999:bi)||a.file.localeCompare(b.file);});const cards=ordered.map(d=>'<li><a href="'+rel(route,d.route)+'"><strong>'+esc(d.title)+'</strong><span>'+esc(d.file)+'</span></a></li>').join('');const launch=p.repo==='MCAT'?'<a class="button button-primary" href="'+rel(route,'study/mcat/index.html')+'">Launch study app ↗</a>':'<a class="button button-primary" href="'+rel(route,entry.route)+'">Start reading →</a>';await save(route,page(route,p.title,'<section class="container project-intro"><p class="eyebrow line-label">'+esc(p.kind)+'</p><h1>'+esc(p.title)+'</h1><p class="hero-description">'+esc(p.description)+'</p><div class="actions">'+launch+'<a class="button button-secondary" href="https://github.com/collingeorge/'+p.repo+'">Source repository ↗</a><a class="button button-secondary" href="'+rel(route,'library.html')+'">Search all documents</a></div><p class="scope-note">'+esc(p.boundary)+'</p></section><section class="container project-documents"><div class="section-heading"><div><p class="eyebrow">Complete reading collection</p><h2>Documents &amp; supporting records</h2></div><p>Full documents with local navigation. Supporting code and records are presented for inspection; files are not executed by reading them.</p></div><ul class="document-list">'+cards+'</ul></section>'));}
// Host the complete MCAT static runtime under its own service-worker scope.
for(const f of await walk(path.join(src,'MCAT'))){const r=pos(path.relative(path.join(src,'MCAT'),f));if(r.startsWith('.github/')||r==='.gitignore')continue;await save('study/mcat/'+r,await fs.readFile(f));}
let app=await fs.readFile(path.join(out,'study/mcat/index.html'),'utf8');app=app.includes('study-masthead')?app:app.replace('</head>','<link rel="stylesheet" href="css/portfolio-theme.css?v=full-html-2"></head>').replace('<body>','<body><div class="portfolio-return"><a href="../../index.html">← Collin B. George · Portfolio</a><a href="../../projects/mcat-study/index.html">Study guide &amp; sources ↗</a></div>');await save('study/mcat/index.html',app);
await save('study/mcat/css/portfolio-theme.css',await fs.readFile(path.join(root,'assets/mcat-theme.css')));let sw=await fs.readFile(path.join(out,'study/mcat/sw.js'),'utf8');sw=sw.replace("const CACHE = '","const CACHE = 'portfolio-html2-").replace("'./', 'index.html'","'./', 'css/portfolio-theme.css', 'index.html'");sw=sw.replace("k.startsWith('mcat-')","k.startsWith('portfolio-html2-mcat-')");await save('study/mcat/sw.js',sw);
// Host the Smart NOA web simulator (static ES modules; tests excluded) at /simulator/.
const simDir=path.join(src,'Smart-NOA-Controller','simulator');if(await fs.stat(simDir).then(x=>x.isDirectory(),()=>false))for(const f of await walk(simDir)){const r=pos(path.relative(simDir,f));if(r.startsWith('tests/'))continue;await save('simulator/'+r,await fs.readFile(f));}
// Preserve the original homepage composition and move reading links in-house.
home=rewrite(home,'CM-2026','index.html','index.html');home=home.replaceAll('downloads/CM-2026/assets/','assets/').replace(/(<a class="nav-source" href=")[^"]+/, '$1https://github.com/collingeorge/CM-2026');home=home.replace(/<link rel="canonical"[^>]*>/,'<link rel="canonical" href="'+config.siteUrl+'/">');home=home.replace('portfolio.css?v=20261029-3','portfolio.css?v=full-html-1').replace('</head>','<link rel="stylesheet" href="assets/reading.css?v=full-html-1"></head>');home=home.replace('<a href="#library">Project library</a>','<a href="library.html">Reading library</a>');
for(const p of projects)home=home.replaceAll('href="https://github.com/collingeorge/'+p.repo+'"','href="'+repoIndex(p.repo)+'"');
home=home.replace('Seven public projects, including an MCAT study app','Eight public projects, including an MCAT study app and a research methods study').replace('>7 projects</p>','>8 projects</p>').replace('data-filter="Study tools" aria-pressed="false">Study tools</button>','data-filter="Study tools" aria-pressed="false">Study tools</button><button type="button" data-filter="Research methods" aria-pressed="false">Research methods</button>');
const hla=projects.find(p=>p.repo==='hla-approximation-glioma');const hlaRow='<article class="library-row" data-category="Research methods"><span class="index">08</span><div><span class="eyebrow">Research methods</span><h3><a href="'+repoIndex(hla.repo)+'">'+esc(hla.title)+'</a></h3><p>'+esc(hla.description)+'</p><p class="boundary">'+esc(hla.boundary)+'</p></div><div class="library-links"><a href="'+repoIndex(hla.repo)+'">Read project ↗</a><a href="'+map.get(hla.repo+'/'+hla.evidence).route+'">Study record ↗</a></div></article>';
home=home.replace('</div>\n<div class="library-note">',hlaRow+'</div>\n<div class="library-note">');home=home.replaceAll('>Repository <span','>Read project <span').replace('>Browse the repository <span','>Project guide <span');for(const p of projects){const rd=map.get(p.repo+'/README.md');if(rd)home=home.replaceAll('href="'+rd.route+'"','href="'+repoIndex(p.repo)+'"');}await save('index.html',home);
const allLinks=documents.map(d=>'<li data-search-title="'+esc((d.title+' '+d.file+' '+(d.project?.title||'Portfolio')).toLowerCase())+'"><a href="'+d.route+'"><strong>'+esc(d.title)+'</strong><span>'+esc(d.project?.title||'Portfolio records')+' · '+esc(d.file)+'</span></a></li>').join('');
await save('library.html',page('library.html','Reading library','<section class="container project-intro"><p class="eyebrow line-label">Reading library</p><h1>Explore the complete collection.</h1><p class="hero-description">Search manuscripts, clinical examples, decision trees, references and supporting records. Every document retains its source revision and scope.</p><label class="search-label" for="document-search">Search documents</label><input id="document-search" type="search" placeholder="Try extubation, CABG, analgesia…" autocomplete="off" data-search-index="search-index.json"><p id="search-status" role="status">'+documents.length+' documents</p><ol id="search-results" class="document-list" hidden></ol><div id="browse-documents"><ul class="document-list">'+allLinks+'</ul></div></section>'));
await save('search-index.json',JSON.stringify(search));await save('SOURCE_INDEX.json',JSON.stringify({date:'2026-11-05',scope:'Full source snapshots of listed public repositories; excludes private repositories and clinical shadowing notes',pins,files:sourceManifest},null,2));await save('source-pins.json',JSON.stringify(pins,null,2));await save('.nojekyll','');
for(const f of ['portfolio.css','portfolio.js','favicon.svg'])await save('assets/'+f,await fs.readFile(path.join(cm,'assets',f)));
for(const f of ['mermaid.min.js','MERMAID-LICENSE.txt','MARKED-LICENSE.md'])await save('assets/vendor/'+f,await fs.readFile(path.join(root,'vendor',f)));
for(const f of ['reading.css','reading.js','mcat-theme.css'])await save('assets/'+f,await fs.readFile(path.join(root,'assets',f)));
await save('404.html',page('404.html','Page not found','<section class="container project-intro"><h1>Let’s find the right page.</h1><p>This address is not in the current reading collection.</p><div class="actions"><a class="button button-primary" href="index.html">Portfolio home</a><a class="button button-secondary" href="library.html">Search the library</a></div></section>'));
await save('robots.txt','User-agent: *\nAllow: /\nSitemap: '+config.siteUrl+'/sitemap.xml\n');await save('sitemap.xml','<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'+['index.html','library.html',...projects.map(p=>repoIndex(p.repo)),...documents.map(d=>d.route)].map(p=>'<url><loc>'+esc(config.siteUrl.replace(/\/$/,'')+'/'+p)+'</loc></url>').join('')+'</urlset>');
try{await save('CNAME',await fs.readFile(path.join(root,'../../CNAME'),'utf8'));}catch{}
const report={documents:documents.length,projects:projects.length,decisionTrees:diagrams,sourceFiles:sourceManifest.length,pins,siteUrl:config.siteUrl};await fs.writeFile(path.join(root,'build-report.json'),JSON.stringify(report,null,2));console.log(JSON.stringify(report));

await save('_headers',await fs.readFile(path.join(root,'security-headers.txt')));
Source and scope

Published from CM-2026/tools/site/build-site.mjs at revision 78bcf6107b11. Full source text is retained; HTML presentation does not establish independent clinical review. See authorship, provenance and review records.