Case study · 2026-07-29
A compliance tool that has to be wrong out loud
I built a free AI Act self-check for Italian SMEs, where being confidently wrong has legal consequences for someone else. The tool is the boring part. I froze the test cases before writing any rule, made the privacy promise a test that fails CI, then pointed adversarial passes at my own public claims. They killed four of them, including a false statement in my own README.
Written with AI assistance; the tool itself was built with it, and its own start screen discloses that it is an AI-based system. The audits referenced here were independent adversarial LLM passes, not third-party human review: no lawyer outside the project has validated the rules, and that remains the largest gap. Every legal date is quoted from the Official Journal text; the code, rules, frozen scenarios and audit guide are public.
The problem with a compliance triage
The AI Act (Regulation (EU) 2024/1689) reaches small businesses that have no compliance function. Its transparency duties begin applying on 2 August 2026, and the Digital Omnibus (Regulation (EU) 2026/1744, published in the Official Journal on 24 July 2026, in force from the 27th) moved several dates. A tool that answers “what applies to me, and by when” is useful. A tool that answers it confidently and wrongly is worse than nothing, because the user cannot tell the two apart.
The design constraint follows: the system must be able to be wrong out loud. Not accurate; no v1 is. It has to be structurally unable to hide a gap. Three mechanisms carry that, and each is a testable artifact rather than a promise.
1 · Freeze the test cases before writing the rules
Fourteen legal scenarios (facts in, expected verdict out, each with its own citation) were written and hash-frozen before any rule existed. The git record shows the scenarios committed at 16:54 and the first rules at 20:56 the same day, four hours later, and exactly one commit in the repository’s history touches that directory: the freeze itself. A checksum file plus an independent copy of the same fourteen hashes in the decision log means a silent edit would have to defeat two records.
What does the freeze actually buy? It provably stops the scenarios being edited to fit the rules, which is the failure mode of every self-graded benchmark. It does not stop the rules being written to fit the scenarios: I could see them while writing. The project’s decision log states this in as many words and lists the counter-measures (author each rule from the statute text first, write a justification for why the opposite verdict would violate the cited provision, anchor every date to a source table instead of retyping it). An auditor put it more sharply than I had: commit timestamps are author-controlled, and the server-side record only begins at the first push. The ordering is evidence, not proof.
2 · Make the privacy claim a test that can fail
The tool runs entirely in the browser, with the Python engine compiled to WebAssembly, so answers about a client’s possibly-unlawful practices never reach a server. That claim is worth exactly as much as its enforcement, so it has two: a Content Security Policy forbidding connections to external origins, and an end-to-end test that drives the whole questionnaire and asserts zero non-self requests and zero requests after load, running in CI on every push. Add an analytics snippet and the gate goes red.
An auditor found the hole in that arrangement: the deploy workflow was independent of the test workflow, so a push whose privacy test failed would still have shipped. Deployment is now gated on the test workflow. The claim was true; I had never checked whether a failing test could block a deploy.
The same pass refused two overstatements I had shipped. A CSP does not stop a top-level navigation, only the data-carrying primitives (fetch, XHR, WebSocket, beacon); and on GitHub Pages the policy can only be delivered in a <meta> tag, so header-only directives are silently ignored. Both are now written on the transparency page instead of left for a reader to discover.
3 · A verdict that refuses to guess
Evaluation is three-valued: true, false, unknown. Unknown propagates, and the invariant is that no “compliant” verdict may rest on an unknown fact, including the case that sinks naive implementations, where any(TRUE, UNKNOWN) evaluates to true in the three-valued logic itself, masking an unknown that could still matter. Those are demoted to “undetermined.” A perturbation harness omits and flips every fact of every frozen scenario in turn, 166 perturbations in all, and not one produced a “compliant” resting on missing data.
Users can answer “I don’t know” to any question; it is a first-class answer that leads to “needs human review.” That trade is deliberate: an undetermined verdict costs the user time, and a false compliant is the only output that can cause loss.
What the adversarial passes caught in my own claims
Publishing corrected work is table stakes; publishing the record of the corrections is the demonstration. Before the launch post went out, an adversarial pass read it against the code. It confirmed 21 findings, four blocking.
- My headline promised an accuracy the design cannot deliver. I had written “never a false compliant.” Fail-closed logic guarantees only that missing facts cannot yield a compliant verdict; it cannot exclude a wrong user answer, a mis-modelled rule, or an obligation the v1 slice does not cover. The app’s own disclaimer says “verdicts may be incomplete or wrong.” I had advertised the opposite of my own product.
- My README described a deployment I did not have. “nessun CDN, nessun server, nessun trattamento di dati” (no CDN, no server, no data processing): false as deployed. GitHub Pages, fronted by Fastly, logs visitor IPs like any host. The answers never leave the browser; the site is still hosted. The audience most likely to notice was exactly the audience I was addressing.
- My disclaimer arrived after the call to action. The link sat at character 363 of the post, “this is not legal advice” at 1,539. For reliance purposes that is the wrong order: a reader who clicked at 363 reached the tool before reading the sentence at 1,539.
- My comparison made three claims about other people’s tools, two of them unsupported. It implied the Commission’s official checker and the Future of Life Institute’s lacked Italian, currency, or data guarantees. Both have Italian; I had never verified how either handles data. Research I had commissioned myself already recorded this, and I wrote the paragraph anyway.
Then five auditor personas (a lawyer, a security auditor, a DPO, an institutional re-hoster, a skeptical engineer) were pointed at the live tool with no context and told to verify the public claims or fail trying. All five concluded they would trust it. All five found defects:
- The results page’s “source repository” link was a placeholder pointing at github.com. The one click a skeptic makes to check my claims landed on GitHub’s homepage.
- The explanation trace printed the truth value of the test under the label of the fact. A user who answered “no, users are not informed” read
interaction_disclosed → TRUE. The trace was technically correct; to the user it said the opposite of the answer they had just given. - Citations rendered as
Art. 50(1)(1)andArt. 5(1)(c)(1): the paragraph concatenated onto an article that already contained it. There is no Art. 5(1)(c)(1), and a lawyer cannot cite that. - The published verification recipe proved nothing: comparing a zip against a checksum file shipped beside it does not detect tampering. The check that means something is rebuilding the archive from source and comparing it with what the live site serves.
Verification took the five of them between 32 and 85 minutes, most of it spent working out where to look. That friction became a short audit guide written from their actual obstacles, whose longest section is titled What this does not prove.
The bug the harness caught that I could not
One more, because it is the cleanest argument for the method. The engine ships as a deterministic archive whose sha256 anyone can reproduce and compare with the live site. It reproduced perfectly on my machine, and CI rejected it on the first push. ZipInfo.create_system defaults to 0 on Windows and 3 on Unix, so identical inputs produced different bytes on different platforms: the “anyone can verify this” property would have been false for every auditor not using Windows. A gate built to catch staleness caught a portability defect I did not know existed. It surfaced only because the gate runs on a machine that is not mine.
What this does and does not show
It is worth being exact about which half of this transfers. Not the rules: they cover a triage slice of one regulation and carry every limit below. What transfers is the arrangement around them. Freeze the test cases before the rules exist. Make the privacy claim a test that fails CI. Let the verdict refuse to guess rather than round an unknown up to compliant. Point the adversarial passes at your own public claims, not only at your code. None of that made the tool correct, and nothing here should be read as saying it did. What it did was surface four blocking findings in my own launch post, the defects five auditor personas found on a live tool they were given no context for, and a portability bug I would not have found by being more careful.
Limits, stated as the project’s spine
- No external human legal review. The rules encode articles I read myself. Adversarial LLM passes are not a substitute for a lawyer, and I do not present them as one. This is the gap I am actively trying to close.
- The freeze is evidence, not proof (above), and the anti-bias protocol it belongs to is self-administered.
- Coverage is a triage slice. Chapter III high-risk obligations are not substantively assessed: the tool gives the deadline and asks whether they are met. Italy’s national AI law (L. 132/2025) is out of scope, and its implementing decrees were still drafts at the time of writing.
- One question is collected but unused: provider-versus-deployer does not yet branch any rule, so role-specific duties are not distinguished.
- Two weeks old, no user testing. A pre-registered protocol exists with frozen success thresholds; no participant has run it yet, so nothing is claimed about usability.
Reproducibility
Tool: matte1782.github.io/ai-act-sme-checker. Code, rules, frozen scenarios, decision log and audit guide: github.com/matte1782/ai-act-sme-checker (EUPL-1.2, re-hostable verbatim). bash scripts/check.sh runs the whole gate: 440 tests, the fourteen frozen scenarios, and the bundle-integrity check. Every applicability date is quoted from the OJ text of Regulation (EU) 2026/1744, a copy of which is in the repository with its hash recorded.
Sources
- Regulation (EU) 2024/1689 (AI Act), OJ L 2024/1689, 12.7.2024.
- Regulation (EU) 2026/1744 (Digital Omnibus on AI), OJ L, 24.7.2026; in force 27.7.2026 (Art. 4: third day after publication).
- Amended Art. 113, third paragraph, points (a), (c), (d) and new Art. 111(4): the applicability dates the tool encodes.
- Commission Guidelines on the definition of an AI system, C(2025) 5053: the basis on which the tool classifies itself as an AI system.
- European Commission, AI Act Service Desk and Compliance Checker (beta), live since 8 October 2025, Italian among its languages.
- Future of Life Institute, EU AI Act Compliance Checker (artificialintelligenceact.eu), not affiliated with the EU.