← Back to the site

Projects

What I have built

Every card states the problem, the technical decisions and the measured result. Five are real tools, online, that you can try in the browser in thirty seconds; the sixth is the site you are looking at. Every number quoted can be checked by opening the sources, which are public.

Case 01 Personal project 2026

This website

Problem

Developer websites all look the same: same template, same animations, same sentences. None of them prove anything — they describe skills instead of showing them. I wanted a site that was the evidence rather than a description of the evidence, and one I could open in front of a procurement manager without having to explain it.

Approach

No framework and no component library: one HTML file per language, one stylesheet, two JavaScript files. You open it, you read it, you change it — and there is no build step to repair a year from now.

The header is a three.js scene. The structure you see is a geodesic derived from a subdivided icosahedron: 162 nodes and 480 edges, obtained by de-duplicating by hand the vertices the library returns repeated. The nodes start scattered off-screen and converge on their exact positions in a little over a second — it is what I do for a living, drawn.

The scene has no palette of its own: it reads its colours from the CSS. When the theme changes, the 3D re-reads the same variables as the page, so the two cannot drift apart. It stops when it leaves the screen, and under prefers-reduced-motion it draws a single frame with the structure already assembled.

Accessibility was verified by measurement, not by eye: a script walks every text element in both themes and computes its contrast ratio against the real background — the one you get after climbing back up the ancestors. 465 elements across the home page and this one, none below the WCAG AA threshold. It found an error that was invisible to the eye: inside the compact menu the button inherited the list colour and ended up grey on brass, 1.01:1.

No calls to external domains to render the page: the typefaces and three.js are hosted here. The only outbound request happens when somebody submits the contact form.

Result

A page that explains itself to anyone who can read the source, and that the people who cannot still remember. Sober enough to send to a company, technical enough not to look bought.

465text elements checked against WCAG AA across both pages and both themes, none below threshold
162nodes and 480 edges computed in the browser every frame
0frameworks, component libraries, build steps
0external domains contacted to render the page
  • HTML
  • CSS
  • JavaScript
  • three.js
  • WebGL
  • WCAG AA
Case 02 Personal project 2026

Norma — citation search over a legal corpus

Problem

An assistant answering from a company's internal documents has a constraint generic assistants do not: it has to cite, not paraphrase. If you cannot trace the exact paragraph, the answer is not verifiable — and an answer nobody can verify is one nobody can act on.

Approach

Exact-citation search over the GDPR, entirely in the browser: no server, no model, no API key. The retrieval unit is the paragraph rather than the article, so the answer is "Article 33(1)" instead of two pages of text.

Not using an embedding model was measured, not assumed: 45 questions written before looking at any results, and an ablation that switches off one correction at a time. The lexical engine reaches 56 % where bare BM25 stops at 18 %, and costs 156 KB instead of 30 MB.

Result

A tool you can try in ten seconds and check line by line — and a costed estimate for the next step, rather than a guessed one.

56 %of questions put the right article first
586passages indexed: 413 paragraphs and 173 recitals
156 KBover the wire, against roughly 30 MB for an embedding model
  • JavaScript
  • Python
  • BM25
  • NLP
  • Information retrieval
Case 03 Personal project 2026

Estratto — Italian e-invoices, readable in the browser

Problem

Since 2019 business-to-business invoicing in Italy is an XML file, and the company receiving it cannot read it: it is markup, not a document. For a quick check people end up uploading it to whatever site they find — a document containing their own clients' data.

Approach

Drop the file and read it in plain language, with the codes translated and a CSV export. Everything in the browser: no request leaves the page, and that is not a promise — you can check it in the network panel.

Beyond displaying it, it checks the arithmetic: the tax authority's schema verifies that fields exist, not that 22 % of 100 is 22. The subtlest check is on the instalments, because under split payment and withholding tax they are legitimately lower than the total. Flagging those as anomalies would have made the tool unusable: the difference between "fine", "note" and "warning" is the product.

Result

Nine fixtures with hand-computed expectations, two deliberately broken. The suite was itself tested by breaking the reader three different ways: all three mutations are caught.

0bytes sent: the document never leaves the reader's machine
113assertions over 9 fixtures, expectations computed by hand
29 KBover the wire, all nine samples included, with no dependencies
  • JavaScript
  • XML
  • FatturaPA
  • Parsing
  • Validation
Case 04Personal project2026

Distinta — made-to-measure product configurator

Problem

It is the thing Italian manufacturing SMEs ask for most and find least. Anyone selling made-to-measure windows or furniture rebuilds the same quote by hand every day, and every impossible combination caught late is an order that comes back from the workshop.

Approach

Incompatibilities are symmetric: pick triple glazing first and the thin frame disappears; pick the thin frame first and the glazing does. A configurator that handles one direction only forces you to backtrack and guess. And every disabled option states its reason — a grey box with no explanation reads as a broken site.

The product is not in the code: options, prices, rules and constraints live in a data file. The technical drawing is to true scale — the units are millimetres — and redraws with its dimension lines on every change.

Result

All 2,560 combinations enumerated, and a full walk of the reachable space proving that nobody, by clicking, can compose a product that cannot be manufactured. The check immediately found a real defect: the catalogue’s own default violated one of the catalogue’s constraints.

0impossible configurations reachable, across 664 states and 10,616 moves
2,560option combinations, every one enumerated
19 KBover the wire, with no dependencies
  • JavaScript
  • SVG
  • Rule engine
  • Quoting
Case 05Personal project2026

Quadro — a dashboard over Italy, on ISTAT data

Problem

A dashboard fed pre-computed totals can only answer the questions whoever prepared the data had anticipated. The first different question needs a change, a release and a week.

Approach

The browser receives the municipal detail — 7,896 rows — and aggregates on the fly. Changing level, indicator or filter is not a request to a server: it is a loop over an array.

The map weighs 99 KB because it is TopoJSON: quantised coordinates, points stored as deltas, and borders shared between neighbouring regions rather than duplicated. The same drawing as GeoJSON weighs 2,700.

Result

Fifty-one checks that reconcile the totals by every route and test them against facts that do not come from the data. On the first run Italy’s population came out at 118 million: the ISTAT file carries a total row per municipality, and summing it with the detail counts everything twice.

7,896municipalities aggregated in the browser on every interaction
99 KBfor the vector map, against 2.7 MB in the naive format
51reconciliation checks passed
  • JavaScript
  • Python
  • TopoJSON
  • Open data
  • Visualisation
Case 06Personal project2026

Soglia — a WCAG contrast checker

Problem

A contrast ratio is a single number, and on its own it decides nothing: the same pair of colours passes at 24 px and fails at 16. The tools you find show the number with two fixed ticks beside it, and the real question goes unanswered — from what size up is this colour fine?

Approach

The threshold moves with size and weight, because that is how the standard is written. Transparency is composited before it is measured: rgba(0,0,0,.5) on white is #808080 and scores 3.95:1, not 21:1 — a tool that ignores alpha returns a number that exists on no screen.

And when it fails, it says which colour to use instead. The fix moves in lightness only, inside OKLCh, so the hue stays put and a corrected red stays red rather than turning pink. Sometimes, though, the right answer is that the background is wrong: on a mid grey no text colour whatsoever reaches 7:1, and saying so is worth more than offering a colour that does not solve it.

The real work is not one pair but a whole theme, where the pairs are n×n: paste the custom properties of a stylesheet and they all come out at once.

Result

A hundred and eighty-one checks, eight of them against numbers published by other people that I did not compute anywhere myself. The expectations are recomputed by a second implementation written in Python from the specification, not by the engine they are meant to test. And the page’s own theme passes its own tool against AAA, not AA: the check re-reads the stylesheet from disk, so a colour changed without looking at the contrast makes it fail.

Measuring the rendered page, rather than the declared colours alone, it found a defect of its own: on the matrix diagonal a grey dash at 12 px sat at 4.05:1. It was decoration disguised as text, and it is now a hatch.

181checks passed, eight of them on values published by others
2independent implementations that have to agree: JavaScript and Python
0elements below WCAG AA out of 162, in both themes: here the bar is AAA
  • JavaScript
  • Python
  • WCAG 2.1
  • OKLCh
  • Accessibility