diff --git a/build/Site.hs b/build/Site.hs index 6c54a7c..023bc38 100644 --- a/build/Site.hs +++ b/build/Site.hs @@ -29,7 +29,7 @@ import Compilers (essayCompiler, postCompiler, pageCompiler, poetryCompiler, fi import Catalog (musicCatalogCtx) import Commonplace (commonplaceCtx) import Now (nowCtx) -import Vita (vitaCtx) +import Vita (vitaCtx, projectsCtx) import Contexts (siteCtx, essayCtx, postCtx, pageCtx, poetryCtx, fictionCtx, compositionCtx, contentKindField, recentFirstByDisplay, tagLinksFieldExcludingTopSegment, isProvedConfidence) @@ -404,7 +404,18 @@ rules = do -- so nginx serves them via index-file resolution and the URLs stay stable -- if the underlying files are later reorganized into co-located directories. -- --------------------------------------------------------------------------- - match "content/cv/*.md" $ do + -- The project index is generated from yaml-source/data/projects.yml — + -- the same file that feeds the CV's and résumé's project sections — so + -- it gets projectsCtx and its own template. The markdown keeps only the + -- page's framing prose. Individual project writeups stay as essays. + match "content/cv/projects.md" $ do + route $ constRoute "cv/projects/index.html" + compile $ pageCompiler + >>= loadAndApplyTemplate "templates/projects.html" projectsCtx + >>= loadAndApplyTemplate "templates/default.html" projectsCtx + >>= relativizeUrls + + match ("content/cv/*.md" .&&. complement "content/cv/projects.md") $ do route $ customRoute $ \ident -> let fname = takeFileName (toFilePath ident) slug = takeWhile (/= '.') fname diff --git a/build/Vita.hs b/build/Vita.hs index 18653e3..f7fb4eb 100644 --- a/build/Vita.hs +++ b/build/Vita.hs @@ -21,10 +21,12 @@ -- for the full supported set and why escaping happens before conversion. module Vita ( vitaCtx + , projectsCtx ) where import Data.Aeson (FromJSON (..), Object, Value (..), withObject, (.:), (.:?), (.!=)) import Data.Aeson.Types (Parser, typeMismatch) +import Data.Char (toLower) import Data.List (isPrefixOf, sortOn) import Data.Maybe (mapMaybe) import Data.Scientific (isInteger, toRealFloat) @@ -62,6 +64,23 @@ reqLoose o k = unLoose <$> o .: K.fromString k optLoose :: Object -> String -> Parser (Maybe String) optLoose o k = fmap unLoose <$> o .:? K.fromString k +-- | Whether an entry appears on this page. +-- +-- The YAML has carried two visibility axes since it drove two documents: +-- @cv_visible@ and @resume_visible@, so the CV and the résumé can disagree +-- about an entry without duplicating it. Generating the vita page from the +-- same data added a third surface, and folding it into @cv_visible@ would +-- have silently collapsed a distinction the file already knew how to make +-- — an entry can be worth keeping on a document handed to a reader while +-- being wrong for a page that is crawled. +-- +-- @web_visible@ therefore defaults to @cv_visible@: existing entries behave +-- exactly as before, and the axis only exists where someone sets it. +webVisible :: Object -> Parser Bool +webVisible o = do + cv <- o .:? "cv_visible" .!= True + o .:? "web_visible" .!= cv + -- --------------------------------------------------------------------------- -- Entry types -- --------------------------------------------------------------------------- @@ -84,7 +103,7 @@ data Edu = Edu , edEnd :: Maybe String , edGpa :: Maybe String , edNotes :: Maybe String - , edVisible :: Bool + , edWeb :: Bool } instance FromJSON Edu where @@ -96,7 +115,7 @@ instance FromJSON Edu where <*> optLoose o "end" <*> optLoose o "gpa" <*> optLoose o "notes_cv" - <*> o .:? "cv_visible" .!= True + <*> webVisible o newtype EduDoc = EduDoc { unEduDoc :: [Edu] } @@ -112,7 +131,7 @@ data Pub = Pub , pbTarget :: Maybe String , pbLinks :: [Link] , pbNote :: Maybe String - , pbVisible :: Bool + , pbWeb :: Bool } instance FromJSON Pub where @@ -125,7 +144,7 @@ instance FromJSON Pub where <*> optLoose o "target" <*> o .:? "links" .!= [] <*> optLoose o "equal_contrib_note" - <*> o .:? "cv_visible" .!= True + <*> webVisible o newtype PubDoc = PubDoc { unPubDoc :: [Pub] } @@ -140,7 +159,7 @@ data Pres = Pres , prYear :: String , prMonth :: Maybe String , prStatus :: Maybe String - , prVisible :: Bool + , prWeb :: Bool } instance FromJSON Pres where @@ -152,7 +171,7 @@ instance FromJSON Pres where <*> reqLoose o "year" <*> optLoose o "month" <*> optLoose o "status" - <*> o .:? "cv_visible" .!= True + <*> webVisible o newtype PresDoc = PresDoc { unPresDoc :: [Pres] } @@ -170,7 +189,7 @@ data Exp = Exp , exOrder :: Int , exPreamble :: Maybe String , exBullets :: [String] - , exVisible :: Bool + , exWeb :: Bool } instance FromJSON Exp where @@ -185,27 +204,54 @@ instance FromJSON Exp where <*> o .:? "cv_order" .!= 99 <*> optLoose o "cv_preamble" <*> o .:? "bullets" .!= [] - <*> o .:? "cv_visible" .!= True + <*> webVisible o newtype ExpDoc = ExpDoc { unExpDoc :: [Exp] } instance FromJSON ExpDoc where parseJSON = withObject "ExpDoc" $ \o -> ExpDoc <$> o .: "experience" +data Proj = Proj + { pjName :: String + , pjGroup :: String + , pjEssay :: Maybe String + , pjStart :: String + , pjEnd :: Maybe String + , pjDescription :: String + , pjLinks :: [Link] + , pjWeb :: Bool + } + +instance FromJSON Proj where + parseJSON = withObject "Proj" $ \o -> Proj + <$> reqLoose o "name" + <*> o .:? "group" .!= "Projects" + <*> optLoose o "essay" + <*> reqLoose o "start" + <*> optLoose o "end" + <*> reqLoose o "description" + <*> o .:? "links" .!= [] + <*> webVisible o + +newtype ProjDoc = ProjDoc { unProjDoc :: [Proj] } + +instance FromJSON ProjDoc where + parseJSON = withObject "ProjDoc" $ \o -> ProjDoc <$> o .: "projects" + -- | @personal.yml@ also carries a @display@ string per link (the value the -- CV prints in full, since paper cannot be clicked). It is deliberately -- not read here — see 'renderContact'. data ProfileLink = ProfileLink { plLabel :: String , plHref :: String - , plVisible :: Bool + , plWeb :: Bool } instance FromJSON ProfileLink where parseJSON = withObject "ProfileLink" $ \o -> ProfileLink <$> reqLoose o "label" <*> reqLoose o "href" - <*> o .:? "cv_visible" .!= True + <*> webVisible o -- | Contact details from @personal.yml@. The phone number is deliberately -- not parsed: it is printed on the CV PDF, which is a document handed to @@ -246,6 +292,10 @@ latexToHtml = . substAll "--" "–" . substAll "$\\times$" "×" . substAll "$\\delta$" "δ" + . substAll "$\\rightarrow$" "→" + -- Approximation, not a non-breaking space. Bare `~` is LaTeX's nbsp, so + -- "~10 crates" silently renders as "( 10 crates" and loses the "about". + . substAll "$\\sim$" "~" . substAll "\\#" "#" . substAll "{,}" "," . substAll "~" " " @@ -371,7 +421,7 @@ renderEducation es , "" ] where - visible = filter edVisible es + visible = filter edWeb es one e = concat [ "
", tex (pjDescription p), "
" + , renderLinks (pjLinks p) + , "" , "" , escapeHtml (pnEmail p) , "" - , concatMap one (filter plVisible (pnLinks p)) + , concatMap one (filter plWeb (pnLinks p)) , "
" ] where @@ -541,3 +635,12 @@ vitaCtx = <> sectionField "vita-contact-html" (renderContact <$> loadYaml "yaml-source/data/personal.yml") <> siteCtx + +-- | The @/cv/projects/@ index. Reuses the vita flag so it picks up the same +-- stylesheets and reads as the same kind of surface. +projectsCtx :: Context String +projectsCtx = + constField "vita" "true" + <> sectionField "vita-projects-html" + (renderProjects . unProjDoc <$> loadYaml "yaml-source/data/projects.yml") + <> siteCtx diff --git a/content/cv/projects.md b/content/cv/projects.md index 9e6f395..524054b 100644 --- a/content/cv/projects.md +++ b/content/cv/projects.md @@ -4,22 +4,6 @@ tags: meta portal: true --- -Index of engineering artifacts. Systems depth is the primary axis of this page; machine-learning and deployed artifacts follow. +Index of engineering artifacts, generated from the same data as the project sections of the [CV](/cv.pdf) and [résumé](/resume.pdf). Systems depth is the primary axis; self-directed tools and deployed machine-learning work follow. -## Low-Level & Systems - -- **[Weenix](/essays/weenix/)**\ - Unix-like kernel in 10,000 lines of C. Virtual memory, VFS, system calls, threading, device drivers, interrupt handlers, and file systems; custom linker support for running userspace x86-64 ELF binaries. Originally a project from Brown CS 169 (Operating Systems with Lab), extended with further features like pipes and userspace preemption. January – August 2025. -- **[Networking Stack from Scratch](/essays/networking-stack/)**\ - TCP/IP, RIP, UDP, and DNS in Go, supporting file transmission of up to 1 GB across networks of 8 virtual machines. Extended with a fully RFC-compliant SSH implementation (2,000+ additional lines) supporting sustained sessions of arbitrary length. October 2024 – July 2025. -- **[Where Does SIMD Help Post-Quantum Cryptography?](/essays/where-does-simd-help-post-quantum-cryptography/)** · [Artifact](https://git.levineuwirth.org/neuwirth/where-simd-helps)\ - Hand-written AVX2 assembly for ML-KEM / Kyber. 35×–56× speedup over compiler-optimized C for core NTT arithmetic; 5.4×–7.1× end-to-end KEM speedup. Full statistical-analysis pipeline (Mann-Whitney U, Cliff's δ, bootstrapped CIs) on Brown's OSCAR HPC cluster. Phase 1 report and reproducible artifact public. -- **[LeVCS](/essays/levcs/)** · [Artifact](https://git.levineuwirth.org/neuwirth/levcs) · [Instance](https://levcs.levineuwirth.org)\ - Distributed version control system in Rust (~10 crates, 194 passing tests at v0.1.0). BLAKE3 content addressing, signed Ed25519 authority chains for protocol-level identity and push authorization, federation as the normal operating mode with three storage modes (full / release / metadata), and a cascading per-file merge engine (textual → format-aware → tree-sitter → wasm plugin) that resolves git's common false conflicts. Substrate complete; workflow surface (PR/review, issues, web UI) deferred. First federation instance at [levcs.levineuwirth.org](https://levcs.levineuwirth.org). April 2026 – present. - -## Machine Learning & Deployed - -- **[ICD-10-CM outcome calculator](https://levineuwirth.github.io/icd_embeddings/)** · [Preprint](/essays/beyond-comorbidity-indices/) · [Code](https://github.com/levineuwirth/icd_embeddings)\ - Public, read-only calculator for the permutation-invariant Deep Sets model underlying the paper currently under review at *JAMIA*. Takes a diagnosis-code set; returns 30-day readmission and postdischarge mortality predictions with Integrated-Gradients attribution. -- **[NeuroPose](/essays/neuropose/)**\ - 3D pose-estimation and kinematic-analysis system for neurological-recovery research in Liqi Shu's laboratory at Brown Neurology. Python/TensorFlow inference pipeline, MATLAB-based statistical post-processing, Rust backend with HTML/JS frontends. 20,000+ lines across four externally-funded sub-projects since 2023. +Where a project has a writeup, its title links to it. Those are essays rather than records — informal, longer, and written in their own voice. diff --git a/static/cv.pdf b/static/cv.pdf index 920a6e0..28d2e99 100644 Binary files a/static/cv.pdf and b/static/cv.pdf differ diff --git a/static/resume.pdf b/static/resume.pdf index 3f7e533..f948eb9 100644 Binary files a/static/resume.pdf and b/static/resume.pdf differ diff --git a/templates/projects.html b/templates/projects.html new file mode 100644 index 0000000..bf1ed48 --- /dev/null +++ b/templates/projects.html @@ -0,0 +1,7 @@ +