Full-Stack Engineer · AI & Automation

I build systems that save real money.

I work across React/Next.js, Python, and .NET — and ship the hard parts: data pipelines, cloud, and real UX.

production apps
5+production appsshipped, and used every day
daily users
30+daily usersacross a 50-person engineering firm
React.NETPythonNode.jsSQLAgentic AI
Résumé
804hours
reclaimed
$130K · 2025–26

2025 · full year243 hrs
2026 · ytd · q2561 hrs
budget automation — 71% of 2026, the rebuild that doubled its per-project yield
  • budget automation
  • delivery management
  • plot automation
  • in-house file explorer
  • element linking
  • cutsheet management
Once per project, on a constant flow of them — so it recurs every year. Modeled from ROI accounting at an average billable rate; not measured.
Hours reclaimed by the tools I built, by year, from ROI accounting
PeriodHours
2025 (full year)243
2026 (ytd · q2)561
Total 2025–26804 hours ($130K)

Flagship · production ecosystem

The software platform behind a 50-person engineering firm

A fragile internal toolset, grown into the platform a 50-person engineering firm runs its week on — one system spanning finance, engineering design, and everything between.

what it spans
budget & workforce forecasting
finance · engineering design · operations · AI

The firm wanted to use AI. Nobody asked what it would have to read. So I built that first — governed data, and documents a machine can process.

01.1The Product

A document-delivery desktop app, shipped as a product

I ship products, not demos. Somebody’s Monday depends on this thing booting.

A WinForms v1 grown into a WPF / .NET 9 rewrite on Clean Architecture + EF Core. A hybrid integration against a commercial ERP — its API for keys, ODBC for detail — pulls ~600 projects in ~2 seconds. 970 tests. An installer that bundles the runtime, a channel auto-updater, and a forced v1 → v2 migration, because the difference between an app and a product is what happens on the machine you will never see.

  • .NET 9
  • WPF
  • EF Core
  • Clean Architecture
  • xUnit
Deliverables, with due-date state, review hold, and ownership
DeliverableDueHoldOwner
Riser diagram · level 3
D-1042
Overdueyours
Load calculation · east wing
D-1043
Due soonyours
Panel schedule · rev C
D-1044
On trackHeld
Specification · fire alarm
D-1045
Due soon
Coordination report · MEP
D-1046
On track
01.2The Decision Engine

Workforce-capacity forecasting

I can model a business, not just build a UI. It pays for itself on a single avoided mis-hire.

A single-user desktop viewer rebuilt as a multi-user authenticated web engine: a Python / FastAPI service fusing a commercial ERP and CRM into three forecast models, shaping raw project demand into bell curves rather than flat monthly averages, against real staff capacity. ~90% less effort than the Excel process it replaced.

90%
effort cut
forecasting vs the old Excel process
  • Python
  • FastAPI
  • Next.js
  • OAuth2
  • Forecast modeling
JFMAMJJASONDcap
flat monthly average ──▶ shaped demand · staff capacity held constant
the honesty audit

Its first honest output was that the firm was 165% utilized. That number was false — the model was right and the inputs were not. I said so before anyone asked.

01.3The Method

Standards written to be read by a model

I build the factory, not just the car.

AI was the goal. This was the prerequisite. An agent reading a repo with no standards document infers the standards from the code — including the mistakes. So I wrote the documents: ~15 of them, ~8,600 lines, two stacks, a routing layer, six subagents. Then I watched a naming rule I had written, committed, and believed in get followed once in twelve tries. An authoritative document that never reaches the keystroke is decoration. What follows is what reaching the keystroke looks like.

every artifact in this repository carries one
2026-07-09NRA-103nrap-brain-analysisreadout
when
ISO, so it sorts
what
the ticket that asked
why
the change, in words
form
research, plan, or readout

The branch carries it. The commit carries it. The section you are reading carries it.You can walk any pixel on this site back to the ticket that asked for it.

000101.101.201.302030405

You have been reading a tagged document for the last four sections.

Also shipped at the firm
  • OCR doc-intelligence over 10,000+ files
  • 7,113-project search · sub-second
  • Company site — Next.js 15 / React 19 / Three.js / Supabase
  • Revit add-in suite · 4 versions (2023–2026)
  • 35+ zero-downtime releases
Deep dive · the method, enforcedHow the provenance actually holds — the governance model, and the mechanism that beats the rule.Read more

Governance, with edges

A standards system is only as real as the things it refuses. These are the edges — the parts a reader can check rather than take on faith.

Deliverable taxonomy
4 tiers, 22 valid combinations, full compatibility matrix
Access control
14 role codes, 4 access levels, most-restrictive default
Versioning
SemVer from 0.1.0; 4 version fields kept in sync by script
ERP integration
3-step session lifecycle; 11 methods documented as non-functional
Design tokens
8 named type tokens, 12 px minimum, 6 spacing values
Agent workflow
6 subagents, 8 commands, a 7-state ticket workflow

The compatibility matrix

64 combinations · 22 legal · derived, not listed

The type was a substring once — bracketed codes like [APR] and [DFT] jammed into the name, where nothing could validate it. I made it a foreign key and the modifiers typed columns. Then the matrix below became answerable.

Every combination of the deliverable type and its four modifiers. Select a cell to see whether it is legal, or every invariant that rejects it.
Deliverable type····h····d··hd····p·h·p··dp·hdp····rh··r·d·rhd·r··prh·pr·dprhdpr
Issuance
Approval
Coordination
Open Item

columns: h = hold d = draft p = peer-review r = requires-specs

Select a cell. The illegal ones are the interesting ones.
  • I1  A peer review is set only on a coordination item — milestones are reviewed at the gate itself.
  • I2  Draft content is set only on milestone types — an issuance or an approval.
  • I3  A draft is never on hold — there is nothing yet to hold.
  • I4  An open item never requires specifications.

Writing the matrix down is what makes “is this state legal?” a lookup instead of an argument. The legal set above is derived from those four rules — change one and the count changes with it, and the build fails until the page agrees.

The finding

Auditing my own document set, I found a live credential stored in plaintext in one of the reference docs — a service username and a password, on a network share the whole team could read, sitting next to the API endpoint it authenticated against. I flagged it for rotation and redaction the same day.

The credential is the least interesting part. The standards already forbade it: the coding standard’s security section mandates a secrets manager and states, in as many words, never commit secrets. A correct rule, written down and read — and a secret still landed in plaintext. That is not a discipline problem, and more discipline will not retire it. It is structural. The doc set knew better than the doc set.

A control that depends on attention fails on the day attention is somewhere else. So the fix belongs at the foundation, not in the margins: enforcement on the commit path, for every author, every time — a standard that scales because it no longer needs to be remembered.Rules rely on attention. Mechanisms don’t.engineering-standards SOP system, §9 — what I’d improve
prose
a standard a person is asked to remember
agent context
a standard a model reads before it writes
executable rule
a standard that fails the commit

That last rung is the one this page stands on. Below is a rule from the same system, running as a mechanism — in your browser now, and on the commit that shipped this section.

The rule, and the thing that keeps it

thoughts/shared/{plans,research}/YYYY-MM-DD-<TICKET>-<slug>.md

filename
The linter decides. This page does not get a vote.

That convention was written down, committed, and correct. Across the twelve artifacts that existed before anything enforced it, it was followed once. The eleven that missed it are still here, grandfathered — a lint that blocks every commit on day one gets disabled on day two.

── pre-commit ──────────────────────────────
✓ lint-filenames: passed
✓ leak-scan: no secrets, no identifiers on shipping surfaces
✓ check-filename-rule: the rendered rule matches the enforced rule
────────────────────────────────────────────

This repository’s pre-commit hook, on the commit that shipped this section. Line two is the scanner the finding above should have had. Line three checks that the rule on this page is the same string line one enforces. Bypass is --no-verify — and if you find yourself typing it, the rule is wrong. Fix the rule, not the commit.

Signature project · quantitative systems

Trader — how I found out my strategy didn’t work

I built an event-driven forex bot on a practice OANDA account, then spent most of the project doing the unglamorous part: pulling eight years of candles down from the broker, pickling them, and testing whether the strategy actually made money. It mostly didn’t. Scroll the pipeline below — collect, store, compute, backtest, report, tune — and you’ll watch the thing that matters get built: not a money printer, but an instrument that tells me the truth about a strategy. Most strategies aren’t true. This one taught me to check.

PRACTICE ACCOUNT

Showing the bands the bot watches. Candles are procedurally generated; the formulas are the bot’s real ones.

  1. Collect

    The broker hands you 3,000 candles per request, so you walk the window forward in time steps until you reach the end date. Three retries per window, because the API drops requests and an eight-year gap in your data is a bug you find months later.

    • Window: 2016-01-07 → 2023-12-31
    • Per request: 3,000 candles
    • Retries: 3
    infrastructure/collect_data.py
    CANDLE_COUNT = 3000
    INCREMENTS = { 'M5': 5 * CANDLE_COUNT,
                   'H1': 60 * CANDLE_COUNT,
                   'H4': 240 * CANDLE_COUNT }
    
    while to_date < end_date:
        to_date = from_date + dt.timedelta(minutes=time_step)
        candles = fetch_candles(pair, granularity, from_date, to_date, api)
  2. Store

    Paginated fetches overlap at the seams, so the same candle arrives twice. Drop duplicates on time, sort, reset the index, freeze it to disk. Pickles rather than CSVs — the dtypes survive, and reload is instant when you are about to read the same frame four hundred times.

    • Pickles: 65 files
    • Instruments: 23
    • Timeframes: M5 · H1 · H4
    infrastructure/collect_data.py
    final_df.drop_duplicates(subset=['time'], inplace=True)
    final_df.sort_values(by='time', inplace=True)
    final_df.reset_index(drop=True, inplace=True)
    final_df.to_pickle(f"{file_prefix}{pair}_{granularity}.pkl")
  3. Compute

    Every indicator is a rolling window over the typical price — no loops, no state. Bollinger bands are a moving average plus and minus a scaled standard deviation. The band is not a prediction; it is a statement about how unusual this price is relative to the last twelve.

    • Indicators: Bollinger · ATR · Keltner · RSI · MACD
    • Signal: close crosses band, open inside
    • Gates: spread ≤ max · gain ≥ min
    technicals/indicators.py
    typical_p = (df.mid_c + df.mid_h + df.mid_l) / 3
    stddev = typical_p.rolling(window=n).std()
    df['BB_MA'] = typical_p.rolling(window=n).mean()
    df['BB_UP'] = df['BB_MA'] + (stddev * s)
    df['BB_LW'] = df['BB_MA'] - (stddev * s)
  4. Backtest

    This is where the project actually happened. I swept the parameter grid and watched almost all of it lose money. The first backtester was too slow to iterate on, so I vectorized it; when that was still too slow I ran the pairs across processes. Speed is not vanity here — it is how many hypotheses you get to kill per evening.

    • Combos swept: 32
    • Profitable: 3
    • Breakeven win rate: 41.7% @ RR 1.4
    simulation/guru_tester.py
    # guru_tester.py  →  guru_tester_fast.py  →  ema_macd_mp.py
    def is_trade(row):
        if row.DELTA >= 0 and row.DELTA_PREV < 0:
            return BUY
        elif row.DELTA < 0 and row.DELTA_PREV >= 0:
            return SELL
        return NONE
  5. Report

    Results go to Excel, one sheet per pair with an embedded line chart, because a notebook cell is where you compute a result and a spreadsheet is where you sit and stare at it until you believe it. The file ma_sim_H1.xlsx is still in the repo. It is 419 KB of me being wrong.

    • Output: ma_sim_H1.xlsx · 419 KB
    • Per pair: trades · total · mean · min · max gain
    • Notebooks: 16 in exploration/
    simulation/ma_excel.py
    chart = book.add_chart({'type': 'line'})
    chart.add_series({
        'categories': [sheetname, start_row, labels_col, end_row, labels_col],
        'values':     [sheetname, start_row, data_col,   end_row, data_col],
    })
    chart.set_title({'name': title})
  6. Tune

    What survives the backtest becomes configuration. The live bot loads it at boot, polls for newly closed candles, and sizes every position so a stop-out costs exactly $20 — whatever the pair, the pip location, or the distance to the stop. And it refuses to open a second position on a pair it is already in, because it once stacked entries all the way down.

    • Risk per trade: $20, always
    • Poll: M1 · newly closed candles
    • Guard: trade_is_open()
    bot/settings.json
    { "trade_risk": 20,
      "pairs": { "EUR_USD": { "n_ma": 12, "n_std": 2.5,
                              "maxspread": 0.0004,
                              "mingain": 0.0014,
                              "riskreward": 1.4 } } }
    
    # trade_manager.py — refuse a second position on the same pair
    ot = trade_is_open(trade_decision.pair, api)
    if ot is not None: return None
  • Collect → pickle → pandas → backtest → tune
  • 8 years of candles · 23 instruments · 3 timeframes
  • Bollinger mean-reversion · risk-sized positions
  • Backtested 32 parameter combos — 3 made money
  • Python · pandas · MongoDB · Flask · React

This runs on an OANDA practice account — no real money has ever been at risk, and I show no P&L because there is none worth showing. The candles in the simulation are procedurally generated; the thresholds, formulas, risk math, and signal logic are the bot’s real ones, ported line-for-line. The backtest result is the honest one: at a 1.4 risk-reward ratio you must win 41.7% of trades to break even, the strategy wins about exactly that, and then the spread takes it negative. That finding is the project.

Read the architecture
  • A paginated collector walks the OANDA API 3,000 candles at a time from 2016-01-07 to 2023-12-31, retrying three times per window, then dedupes on timestamp, sorts, and writes one pickle per instrument-timeframe — 65 files across 23 instruments and M5/H1/H4.
  • Indicators are pure pandas over those frames: Bollinger bands on the typical price, plus ATR, Keltner channels, RSI, and MACD — each a rolling window, no loops.
  • The backtester grew up in public: a straightforward guru_tester.py, then a vectorized guru_tester_fast.py when it got too slow to iterate on, then ema_macd_mp.py when even that needed multiprocessing across pairs.
  • Results land in Excel via xlsxwriter — one sheet per pair with an embedded line chart — because a spreadsheet is what you actually stare at when you are deciding whether a strategy is real.
  • The winning parameters become bot/settings.json, which the live bot loads at boot. It polls for newly closed M1 candles, computes bands, checks spread and minimum gain, sizes the position so a stop-out costs exactly $20, and refuses to open a second position on a pair it is already in.

Signature project · real-time systems

Orbo — a music engine built like an instrument

Orbo looks like a music visualizer. Build one properly and you find out what it actually is: a real-time data system — a lossless 48 kHz feed off the OS mixer, a windowed feature pipeline, a lock-free hand-off between two clocks, a renderer on a 16.7-millisecond budget, and a CI rig that diffs the output pixels. This section is the engineering story in five acts: what’s being built, the challenges in the data path, the calls made under constraint, why the foundation holds, and where it’s headed. The music is just the workload.

The system

A real-time data system that happens to make pictures.

Strip the label off and here is what’s actually being built: a streaming pipeline that ingests a lossless 48 kHz feed from the OS mixer, windows it into features, publishes immutable snapshots across a thread boundary, consumes them at 60 frames per second, renders, and verifies the output in CI. “Music visualizer” is the domain. Every shape inside it is the shape of any data system that has to be both right and fast.

The demo here is the same pipeline running in the browser — a TypeScript port of the engine’s real math over a synthesized track, muted by default.

  • In: 48,000 samples/s
  • Features: ~23 snapshots/s
  • Reads: 60 fps · 0 locks
  • Out: verified frames

in your stack · A streaming ETL with a real-time consumer — producer/consumer over immutable snapshots, windowed feature extraction, contract-tested output. It’s Rust here; the same system draws the same in C# or Python.

SIMULATED FEED · REAL MATH

Synthesized audio, analyzed live — the band edges, beat rule, and gain-control constants are the engine’s real ones, ported from Rust.

The data path

48,000 samples a second, no locks, no backlog.

  1. system mix · f32 stereo · 48 kHz
  2. 01Capturemono samples · 48,000/s
  3. 02Analyze1 AudioFeatures / 2,048-sample hop
  4. 03Hand-off1 pointer load / frame
  5. 04RenderHDR Rgba16Float scene
  6. 05Postsame scenes, headless
  7. 06Verify
  8. golden PNGs · shader hashes

The stream does not care about your architecture: audio callbacks arrive in device-sized batches, the render loop runs on its own clock, sample rates differ per machine, and loudness differs per track. Each of those is a bug you ship unless you design for it.

So the hop buffer drains with a while, not an if — an early version processed one hop per callback, and a device that batched callbacks grew the backlog and the latency without bound. The thread boundary is a single atomic pointer swap. Every smoothing constant is a per-second rate converted through the hop duration. And gain control normalizes each band against its own tracked peak, so a quiet master and a loud one drive the same show.

the callDrain the hop buffer with while, not if.
instead ofProcessing one hop per audio callback — the first version.
becauseA device that batches callbacks delivers more than one hop at a time; the backlog (and the latency) grew without bound. The fix is one keyword, and there is a regression test named after the bug.

bounded latency

the callOne ArcSwap: the audio thread stores a fresh snapshot per hop, the render thread loads one pointer per frame.
instead ofA Mutex around shared state, or a channel with a queue between the threads.
becauseThe audio callback and the render loop must never block each other — a lock held at the wrong moment is a dropout or a hitch. A pointer swap is wait-free on both sides and allocates nothing on the hot path.

locks taken: 0

the callEvery smoothing constant is a per-second rate, converted through the hop duration at runtime.
instead ofPer-hop constants tuned by feel on one machine.
becauseA 96 kHz mixer ran every envelope ~2× fast — same code, different device, different show. Per-second rates eliminated the bug class, and a test now drives 44.1/48/96 kHz and asserts the release curves agree.

pinned by test

in your stack · The hop buffer is a ring buffer with drain-until-caught-up backpressure (the Kafka-consumer shape). ArcSwap is Interlocked.Exchange on an immutable record. The sample-rate rule is “never bake environment constants into logic” — pinned by a parametrized test.

crates/orbo-audio/src/fft.rs
/// Push mono samples; returns one `AudioFeatures` per completed hop.
pub fn push(&mut self, mono: &[f32]) -> Vec<AudioFeatures> {
    self.buffer.extend_from_slice(mono);
    let mut out = Vec::new();
    while self.buffer.len() >= FFT_SIZE {
        let (bands, energy, beat, surge, spectrum) =
            self.fft.process(&self.buffer[..FFT_SIZE]);
        // …
        self.buffer.drain(..HOP_SIZE);
    }
    out
}
// audio thread: features.store(Arc::new(f))
// render thread: features.load() — lock-free, no allocation

Decisions under constraint

A 16.7 ms budget makes every choice honest.

Sixty frames a second means the whole system answers in 16.7 milliseconds — every frame, forever. A budget is not a goal; it is a forcing function, and it forces the discipline that transfers anywhere: measure before optimizing (a per-pass GPU profiler ships inside the engine), know the cost model of your algorithm, and shape data on the cheap side of the boundary.

The receipts below are measured, not estimated.

the callThe 42 asteroids are impostor quads whose fragment shader reconstructs a sphere’s normal and depth.
instead ofAdding them to the raymarched distance field like everything else.
becauseMeasured, not guessed: a dozen marched rocks cost 8–10 ms; 42 impostors cost 0.3 ms — a sphere-tracer pays for field density, not primitive count. The GPU profiler that produced the number ships in the engine.

9 ms → 0.3 ms

the callA form is a parameter vector; morphs lerp the vector on the CPU, so the shader only ever marches one shape.
instead ofA GPU field morph — mix(d_a, d_b, t) between two distance fields.
becauseThe field morph evaluates both forms on every one of ~91 map() calls per pixel — it doubles a 13 ms scene exactly when it can least afford it. The lerp costs the same mid-melt as at rest.

flat frame cost

the callOne peak driver — grade_intensity — shaped on the CPU with a 70 ms attack / 500 ms release envelope.
instead ofDriving the warm accent and streaks straight off the audio energy in the shader.
becauseThe raw signal steps at hop rate (~43 ms) and a fragment shader has no memory to smooth it — the accent strobed. Shaped once on the CPU, a hit reads as one optical event instead of three effects firing.

no strobe, by design

the callBloom only what is brighter than display white, and pull exposure down at peaks.
instead ofBlooming the whole frame with the strength turned down, and letting peaks clip.
becauseWhole-frame bloom is a milky wash that flattens contrast, and a clipped peak is a white blob. Bright-passed, glow is something a pixel earns — and a hit surfaces as contrast and streak length, not clipping.

glow is earned

in your stack · Profile first, then argue. Know what you actually pay for — the tracer’s cost is field density, not primitive count, the way your ORM’s cost is round-trips, not rows. And do the work at write time so reads stay fast.

crates/orbo-render/shaders/tonemap.wgsl
// ACES filmic tone mapping
fn aces_tonemap(x: vec3<f32>) -> vec3<f32> {
    let a = 2.51;
    let b = 0.03;
    let c = 2.43;
    let d = 0.59;
    let e = 0.14;
    return clamp((x * (a * x + b)) / (x * (c * x + d) + e),
                 vec3<f32>(0.0), vec3<f32>(1.0));
}

The foundation

How you unit-test a picture — and everything else.

The part most side projects skip. Behavior is tested where it matters: the DSP envelopes are asserted identical across 44.1, 48, and 96 kHz; the decision engines — the beat gate, the director’s arm-on-timer-fire-on-onset rule — have unit tests; and the pictures themselves have goldens. Every scene renders headless in CI and is diffed against committed PNGs, with a manifest that hashes each scene’s shader sources so a stale golden is a red build, not a rotten repo.

The process is auditable too: project rules written down with their rationale, docs updated in the same branch as the code they describe, and one ticket per change from research through plan to merged PR.

  • CI: fmt · clippy -D warnings · tests
  • Goldens: every scene · calm + peak
  • Freshness: shader hash · no GPU
  • Process: 23 PRs · NRA-117→147
the callCommit golden PNGs of every scene plus a manifest that hashes each scene’s composed shader source; CI recomputes the hashes with no GPU.
instead ofEyeballing the app before merging and hoping nothing else moved.
becauseLook changes become reviewable diffs — compare fails when >0.5% of pixels differ by >3/255 and writes amplified heatmaps — and check-fresh turns “forgot to regenerate the goldens” into a red build instead of a rotten repo.

the picture is tested

in your stack · Goldens are approval tests — Verify in .NET, pytest-snapshot in Python. The manifest is fixture invalidation by input hash. The project rules are ADRs plus a Definition of Done that includes documentation.

crates/orbo-app/src/snapshot.rs
/// `--check-fresh`: recompute shader hashes and diff against the
/// manifest. No GPU — safe on headless CI runners; red when a
/// shader changed without regenerated snapshots.
pub fn check_fresh() -> Result<()> {
    let manifest = read_manifest(Path::new("scenes/images/manifest.txt"));
    let mut stale: Vec<&str> = Vec::new();
    for (_, key) in SCENES {
        match manifest.get(key) {
            Some(recorded)
                if *recorded != hash_str(&scene_shader_concat(key)) =>
                    stale.push(key),
            // …

The dream

The visualizer was the excuse; the engine is the asset.

Where it’s headed: the engine is becoming a creature that hears. The behavior stack already shipped as tested sockets — typed per-layer contributions whose channel exclusivity is enforced at compile time, a tanh amplitude budget so layers bend rather than clip, leased one-shot actions with smoothstep blend so nothing pops. The behaviors that fill those sockets are specced one sheet each — the tell, the numbers, the math, the failure modes — and land next, with tempo tracking behind them and a desktop release as the long game.

What’s deferred is deferred honestly: the placeholder layers are labeled in the code, macOS capture is parked in the roadmap, and the README’s 4K claim gets called aspirational on this very page.

  • Behavior spine: shipped + tested
  • Specs: one sheet per behavior
  • Stubs: labeled, never hidden

in your stack · A roadmap is cheap — a roadmap with tested extension points is architecture.

  • Capture → FFT → ArcSwap → render → post → CI
  • 4096-pt FFT · Hann · 6 bands · beat + AGC
  • 6 GPU scenes behind one Visualizer trait
  • Dual-Kawase bloom → ACES · HDR pipeline
  • Golden-image CI — headless, no GPU needed
  • Rust · wgpu · WGSL · rustfft · cpal

The engine is a native Rust/wgpu desktop app — what you see on this page is not the engine. It is a TypeScript re-implementation of the engine’s real math: the band edges, the beat rule, and the gain-control constants are ported line-for-line and run over a procedurally generated signal. The repo is private while it’s in active development, so the numbers here come from a source-verified audit of the code rather than a public link. The 4K claim in the project’s own README is aspirational; the measured budget is 720p at 60 fps, and I’d rather tell you that than round up.

Read the architecture
  • Capture is WASAPI loopback: cpal opens an input stream on the default output device, so the engine hears exactly what the speakers get — any app, zero setup. Each callback mixes the frame to mono and feeds a shared hop buffer.
  • Analysis is a 4096-point FFT with a Hann window advancing 2048 samples per hop (50% overlap), folded into six bands along real edges from 20 Hz to 20 kHz. Gain control tracks a per-band peak with instant rise and a ~14.8-second half-life decay; every rate is expressed per second and converted through the hop duration, so the envelopes behave identically at 44.1, 48, and 96 kHz — and a test proves it.
  • The thread hand-off is a single ArcSwap: the audio thread stores a fresh AudioFeatures snapshot per hop, the render thread loads the latest pointer each frame. No locks, no channel, no allocation on the hot path — and the accumulate loop drains every due hop per callback (while, not if), so latency stays bounded no matter how the device batches its callbacks.
  • Rendering is six scenes behind one Visualizer trait — the hero is an analytic-SDF atom with morphing forms and a 60k-star dissolve. A per-pass GPU timestamp profiler found the asteroid field costing 9 ms; re-rendering the asteroids as impostors cut it to 0.3 ms.
  • The post chain runs in HDR: a five-mip dual-Kawase bloom feeds an ACES filmic tonemap, then chromatic aberration, an OLED-black color ramp, and a beat-driven grade whose attack/release envelope is shaped on the CPU so the image can never strobe.
  • Verification is the part most demos skip: a snapshot tool renders every scene headless at calm and peak into committed golden PNGs, a manifest stores a hash of each scene’s shader sources, and CI fails if a shader changed without regenerated snapshots — that check needs no GPU, so it runs on a plain runner next to fmt, clippy -D warnings, and the behavioral test suite.

Signal routing — the uniform slot map

Every scene and the final grade read the same four 16-byte-aligned vec4s, written once per frame. This table is the repo’s actual interface doc (docs/REFERENCE.md), and the repo rule is that any interface change updates the table in the same branch as the code.

group(0) Uniforms — vec4 slots · crates/orbo-render/src/uniforms.rs
Field.x.y.z.w
data0time (s)energy (0–1, AGC)beat_pulse (decays)resolution_x
data1resolution_ysurge (rel. peak)flow (integrated)beat_age (s)
bands0sub_bassbassmidshighs
bands1presenceairgrade_intensity(free)
  • bands1.w is the only free slot left — the rule is to reuse pad floats before growing the struct, and to think twice before doing that.
  • Shaders derive one master driver: intensity = max(surge, beat). Density, full-spectrum color, brightness, and fast motion are all gated behind it — every scene is calm at rest by rule.
  • flow is integrated musical time (flow += (1 + energy) · dt). The rule "never write angle = time × rate" exists because that bug shipped: changing a rate rescales all accumulated time, the camera jumps, and it was mistaken for a performance problem.
  • beat_age idles at 10.0, never 0 — age zero means a beat is firing right now, and the in-shader spring exp(−damp·t)·sin(ω·t) is maximally kicking at t=0. The beat golden is captured 0.12 s after a single onset, because a held beat pins age to 0 and shows nothing.

The audio contract

One struct crosses the thread boundary per 2048-sample hop; everything downstream is a pure function of it. Change a field here and the reference table changes in the same branch — that is what keeps the docs honest.

AudioFeatures — crates/orbo-audio/src/features.rs
FieldTypeMeaning
bands[f32; 6]AGC-normalized band energies 0–1, attack/release smoothed
energyf32weighted overall energy — computed from unsmoothed bands
surgef32(energy − rolling avg) / (1 − avg) · ~1.8 s baseline — the peak signal
beatboolonset this hop (~43 ms @ 48 kHz)
waveformVec<f32>512 raw time-domain samples
spectrumVec<f32>512 AGC-normalized FFT magnitudes
timef64seconds since start — sample-derived, not wall clock
  • Every smoothing constant is a per-second rate converted through the hop duration — and a test drives the same signal at 44.1, 48, and 96 kHz and asserts the release envelopes agree. Sample-rate independence is enforced, not assumed.
  • The AGC’s side effect is documented instead of hidden: silent bands read ~0.1, not 0, because each band is normalized against its own noise floor.

CI — what fails the build

Full checks run on Windows, the primary platform; Linux and macOS are build-only portability legs. Every step exists because it catches a failure somebody actually hit.

.github/workflows/ci.yml
JobStepWhat it catches
checks (windows)cargo fmt --all --checkformatting drift
checks (windows)cargo clippy --workspace -- -D warningswarnings promoted to errors — none accumulate
checks (windows)cargo test --workspaceDSP envelopes · director arming · ring collisions · creature budgets
checks (windows)cargo build --release --workspacethe app and the offline asset tools
checks (windows)cargo run --release -- --check-fresha shader changed without regenerated goldens — no GPU needed
build (ubuntu · macos)cargo build --release --workspaceportability; the Linux leg installs libasound2-dev for cpal
secretsgit ls-files + git grep patternsa tracked .env or key-shaped string fails CI, not code review
  • A concurrency group cancels superseded runs — pushed twice, pay once.
  • A pre-commit hook mirrors the cheap checks locally: staging .env or a key-shaped string blocks the commit; staging a .wgsl without the snapshot manifest warns. Fast on purpose — no cargo calls in the hook, CI runs the heavy ones.

Golden images — how you unit-test a picture

The render path has regression tests the same way the code does. Three commands, three jobs: capture the truth, diff against it, and prove nobody forgot to.

snapshot tooling — crates/orbo-app/src/snapshot.rs
CommandWhat it doesWhen it runs
--snapshotrenders every scene headless at calm + peak (plus a morph frame and a beat frame at beat_age ≈ 0.12 s) into committed golden PNGsafter any look change
--comparerenders fresh and diffs against the goldens — fails when >0.5% of pixels differ by >3/255; writes amplified heatmaps for reviewbefore shipping a look change
--check-freshrecomputes a hash of each scene’s shader sources against scenes/images/manifest.txtevery CI run — no GPU
  • The manifest hashes the composed shader source. WGSL has no #include, so shared preludes are concatenated in — hashing anything less would let an edit to shared ring math rot every dependent golden silently.
  • Scenes resolve by name, never by index — one scene only registers when its assets exist, and a hard-coded index would silently photograph the wrong scene on a clean clone.
  • Cross-frame accumulators (the grade envelope, beat_age) are explicitly reset before each capture — otherwise goldens depend on the order the scenes were rendered in.

Docs that can’t rot

The repo’s CLAUDE.md defines Done, and Done includes documentation: builds clean, visually verified by running (not just compiling), docs match reality in the same branch, the status doc updated, the ticket advanced. Five docs, five jobs — deliberately not blurred.

five docs, five jobs — CLAUDE.md
DocJob
docs/REFERENCE.mdinterfaces: uniform slots, the audio contract, per-scene params, CLI, external APIs, file formats — updated in the same branch as any interface change
docs/STATUS.mdthe living state: what exists, what works, what’s known-broken — a limitation goes in the day you learn it
docs/creature/one behavior sheet per subsystem: the tell it sells, the numbers, the math, failure modes, tuning protocol
thoughts/direction/the mission — amended in place; when anything disagrees with it, it wins or gets deliberately amended
thoughts/research + plansdated per-ticket history — never edited after the fact
  • The bar for a status entry is written down: say why the dead end is inherent and what to do instead — "an SDF can’t represent a thin sheet; more voxels makes it worse; build it procedurally instead," not "SDF has issues."

Ticket-driven, not vibe-driven

The process is auditable: every change traces from a ticket through research and a plan to a merged PR, and the rules that govern it cite the ticket that established them.

  • Branch per ticket using Linear’s generated branch name; the ticket is referenced in every commit message.
  • Each ticket walks research → plan → implement, with the matching Linear comment posted at each step.
  • The July push is NRA-117 through NRA-147 — 23 merged PRs, each with dated research and plan docs kept in the repo under a fixed naming scheme, frozen after the fact.
  • The project rules open with "they exist because we learned them the hard way" and cite the ticket that established them — process that was paid for, not pasted in.

Signature project · agentic systems

jarvis — an agentic loop that hears, reasons, and acts

waking…
  1. Acquire“Hey Jarvis”
  2. UnderstandWhisper
  3. ReasonLLM
  4. DeliverStreaming TTS

Auto-playing the loop — the orb’s state lights the matching stage. Tap “Act” to see the tools.

The same loop, in production at the firm

  1. AcquireERP (ODBC) + CRM (REST)
  2. PipelineAccess → SQL Server
  3. Search7,113 projects · 10k OCR docs
  4. Infer3-model forecast ensemble
  5. ActWPF / .NET · 30+ daily users
  6. Dashboard12-month KPI · Power BI
  7. Analyze$130K · ~90% effort cut

I built jarvis as a local voice assistant to prove the full agentic loop end-to-end: it hears you, reasons with a local or cloud model, calls real tools to act, and answers in under a second. The voice is just the demo — point that same loop at a business and the “act” step becomes forecasting finances, an employee portal, inventory, or QC on CAD models. That’s the pattern I run in production at the firm: data from two business systems → a 3-model forecast → dashboards 30+ people use daily.

  • Acquire → reason → act → deliver
  • Multi-LLM routing — local Ollama or cloud
  • Tool-calling that takes real action
  • Same loop in production: 3-model forecast · 7,113-project search
  • $130K reclaimed in production

jarvis itself is a local, personal build — a working proof of the loop, not a business product. The production numbers here are real: they’re from the platform I built at the firm, which runs this same acquire → reason → act pattern. Business tools shown beyond what’s shipped are illustrative of where the pattern goes.

Read the source on GitHub
Read the architecture
  • One interface sits in front of multiple model backends — a local Ollama model or a cloud LLM — so the same agent loop runs offline or online.
  • Whisper transcribes speech, the model decides on tool calls, and those tools act on the machine: the agent does things, it doesn’t just chat.
  • Replies stream into text-to-speech sentence-by-sentence, so the first audio lands in under a second instead of after the whole response.
  • Two-phase barge-in lets you interrupt mid-sentence, and the entire stack stays within a 16 GB memory budget on a Mac Mini.
  • The same loop, in production at the firm: a commercial ERP (ODBC) and CRM (REST) feed a 3-model forecast that lands on Power BI dashboards 30+ people use to plan staffing.

Signature project · AI infrastructure

NRAP — real-time brain-activation inference, rendered live

20,484 vertices → 7 functional groups, grouped by cortical position

worker.aggregate()   in    (t, 20484) float32      20,484 values   81,936 B = 80.02 KiB/timestep        └── static vertex→region lookup, precomputed            mean · peak · σ  per region  out   59 regions x 3 stats          177 values     ↓ 115.7x   raw → gzip → postgres BYTEA     full fidelity, cold  roi → postgres rows             what the frontend reads
uploading…
Input: 60 s instrumental clip
20,484 vertices

Peak: Default mode — driven by overall RMS energy

  • Auditory
  • Language
  • Emotion
  • Visual
  • Motor
  • Cognitive
  • Default mode
  1. UploadAudio · video · text
  2. EnqueueFastAPI → Redis
  3. Aggregate20,484 verts → 59 regions
  4. RenderThree.js stipple brain

Scripted recreation of the request path, not a live inference — a real run takes 30–60s on a rented A10G. Tap “Infer” to see the backends. The band → region mapping is NRAP's own, ported unchanged; only the band values are a deterministic stand-in. Nothing is playing. The sweep and the scatter are depictions — the worker collapses all 20,484 vertices at once, by a static lookup, and nothing comes apart.

Upload a song, a video, or a paragraph, and NRAP predicts how a brain would respond to it — then renders that response as a 3D particle brain that lights up in real time. Under the visual is the part I actually care about: Meta’s TRIBEv2 foundation model running on a rented GPU that costs $0 when nobody is using it, behind an interface that lets me swap the whole inference engine without the frontend noticing.

  • GPU inference at ~$0.02/clip · scales to zero
  • 3 swappable backends behind one output contract
  • Meta TRIBEv2 on an on-demand Modal A10G
  • 20,484 vertices → 59 regions, aggregated at write time
  • FastAPI + Postgres + Redis queue + worker

The brain above is a scripted recreation of the request path, not a live inference: the region mapping, the band-to-region affinities and the 20,484 → 59 aggregation are NRAP’s real code paths, but the activation values driving it are a deterministic stand-in rather than recorded model output, and nothing runs on a GPU in your browser. A real run takes 30–60 seconds on a rented A10G. NRAP itself is a working MVP, not a product: I built it to learn production ML infrastructure end-to-end. The predictions come from Meta’s TRIBEv2 research model and are not a clinical claim about any individual brain. TRIBEv2’s weights are CC-BY-NC, which is why the commercial-clean `features` backend exists alongside it.

Read the source on GitHub
Read the architecture
  • Three inference backends — a mock generator, a librosa feature-mapper, and the real Meta TRIBEv2 model — all satisfy the identical output contract: a (timesteps, 20484) float32 array. Switching between them is one environment variable and zero frontend changes.
  • TRIBEv2 runs on a rented Modal A10G that spins up on demand and scales back to zero, so idle cost is $0 and a 60-second instrumental clip costs about two cents. Routing skips the expensive extractors when the input does not need them — no video model for an audio file, no language model for instrumental music.
  • The API never calls the GPU directly. It enqueues onto Redis and a separate worker consumes the queue, so a GPU crash degrades throughput instead of taking the service down, and the two scale independently.
  • Vertex-to-region mapping is static, so the worker aggregates 20,484 raw vertices down to 59 stored regions at write time. Reads stay fast; the raw arrays are still kept, gzipped, for full fidelity.
  • The frontend samples a GLB mesh into a particle field and drives it from the activation timeseries — a stipple brain that fires as the inference results stream in, without shipping a shader-heavy model to the browser.

Portfolio

Selected work

A through-line of real products — most of them thread AI into something people actually use.

DevOverflow — live Q&A with AI answersLive demo AI-integrated

DevOverflow — live Q&A with AI answers

A Stack-Overflow-style Q&A platform: post a question and an LLM auto-drafts an answer alongside community responses.

What's hard: Genuinely deployed and clickable — real auth, Next.js, live on Vercel.

Nike Landing PageLive demo

Nike Landing Page

A focused, responsive marketing page built as a front-end craft sample.

What's hard: Pure responsive-UI craft — framed as exactly that, nothing more.

ThinkWell / LunaTeam project · my piece: Luna AI-integrated

ThinkWell / Luna

Cross-platform iOS + Android wellness app with peer-to-peer messaging and activity tracking. I built Luna — a ChatGPT-powered companion — solo.

What's hard: Luna threads OpenAI through a C#/Xamarin MVVM app; the Google Places finder, P2P messaging, and cross-platform build are the parts that genuinely shipped.

  • csharp
  • xamarin
  • android-color
  • apple-tile
  • sqlite-icon
Private / no public demo
Trader — algorithmic OANDA botPractice project

Trader — algorithmic OANDA bot

Event-driven forex bot trading a practice OANDA account with Bollinger-band mean-reversion, risk-based position sizing, and a React dashboard.

What's hard: Risk-sized positions across an ~80-pair engine; the real stack is Python / pandas / OANDA, not the old tag-soup.

  • python-icon
  • re
  • git
Private / no public demo

Get in touch

Let's talk

Looking for a full-stack engineer who ships and measurably saves money? I'm open to work.