<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adi CCH</title>
    <description>The latest articles on DEV Community by Adi CCH (@adi_cch).</description>
    <link>https://dev.to/adi_cch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4020235%2F69f884ff-7807-49b6-bdf3-cdd57f4ccf9d.png</url>
      <title>DEV Community: Adi CCH</title>
      <link>https://dev.to/adi_cch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adi_cch"/>
    <language>en</language>
    <item>
      <title>Turning a compliance site's data layer into two open datasets</title>
      <dc:creator>Adi CCH</dc:creator>
      <pubDate>Wed, 08 Jul 2026 09:06:59 +0000</pubDate>
      <link>https://dev.to/adi_cch/turning-a-compliance-sites-data-layer-into-two-open-datasets-2b9c</link>
      <guid>https://dev.to/adi_cch/turning-a-compliance-sites-data-layer-into-two-open-datasets-2b9c</guid>
      <description>&lt;p&gt;When you build an information site in a high-stakes domain, the hard part is not the prose. It is the data underneath it: the numbers that have to be right, stay right, and be traceable to something official. I have been building Canton Compliance Hub, a free multilingual resource for small businesses in Switzerland, and the most reusable thing to come out of it was not an article. It was the structured data behind them, which I have now published as two open datasets under CC BY 4.0.&lt;/p&gt;

&lt;p&gt;Here is what they are, and the small engineering decisions that made them worth publishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two datasets
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Swiss minimum wage by canton.&lt;/strong&gt; Switzerland has no national minimum wage, which surprises almost everyone hiring here for the first time. Pay floors come from three different places. Five cantons set a statutory cantonal minimum (Geneva is the highest in the world at CHF 24.59 per hour in 2026, then Basel-Stadt, Jura, Neuchatel and Ticino). Three cities set a communal minimum (Zurich, Winterthur, Lucerne). Everywhere else there is no statutory figure at all and pay is governed by sector collective agreements. The dataset captures all 26 cantons plus the three city minimums, with the legal basis and an official source URL on every row. The full comparison is on the &lt;a href="https://cantoncompliancehub.ch/resources/swiss-minimum-wage-by-canton" rel="noopener noreferrer"&gt;Swiss minimum-wage-by-canton table&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swiss SME compliance-deadline calendar.&lt;/strong&gt; The recurring federal deadlines a small company faces across a year: quarterly VAT returns (filed within 60 days of each quarter-end), the annual AHV salary declaration, the general meeting and accounts approval within six months of the financial year-end, personal and corporate tax returns, monthly withholding tax and the 30-day dividend withholding tax. Twelve rows, each with the cadence, who it applies to, and the official source. It backs the evergreen &lt;a href="https://cantoncompliancehub.ch/resources/compliance-calendar" rel="noopener noreferrer"&gt;SME compliance calendar&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: every row cites its own source
&lt;/h2&gt;

&lt;p&gt;The single rule that made this data trustworthy is that a figure with no source URL does not go in. Each row carries &lt;code&gt;source_name&lt;/code&gt; and &lt;code&gt;source_url&lt;/code&gt; pointing at the specific cantonal, municipal or federal page it came from, HEAD-checked to return 200. This is the same discipline the site uses internally (every rendered fact traces back to a government page), exported to the row level. It means a reuser can verify any number without trusting me, which for legal-adjacent data is the whole game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: two formats, not one
&lt;/h2&gt;

&lt;p&gt;Each dataset ships as both CSV and JSON, and they are not the same file twice.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;CSV&lt;/strong&gt; is flat, English-column, one row per canton or deadline. It opens in a spreadsheet and is what most people actually want.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;JSON&lt;/strong&gt; is the source of truth and carries the full English, German and French text for every field, plus metadata the flat file drops.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A tiny &lt;code&gt;build_csv.py&lt;/code&gt; derives the CSV from the JSON, so the flat file can never drift from the multilingual source. Edit the JSON, re-run the builder, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: fold the names for portability, render them properly
&lt;/h2&gt;

&lt;p&gt;The files ASCII-fold canton names (Geneve, Neuchatel, Zurich) so they survive being opened in a naive CSV reader with the wrong encoding. The live site renders the proper native spellings; the portable artifact optimises for not-getting-mangled. Small thing, but "why is my data full of question marks" is a real support cost you can design out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: a license that forces attribution
&lt;/h2&gt;

&lt;p&gt;CC BY 4.0 is deliberate. CC BY requires attribution, which is exactly the point of publishing open data as a content site: anyone who reuses the table has to credit and link the source. The license does the link-building that the data quality earns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;Two reasons. First, the data was already structured and grounded for the site, so exporting it was nearly free. Second, a source-linked comparison table is genuinely useful to journalists, HR teams and other builders, and useful-and-citable beats promotional every time. If you run a self-check on your own situation, the same underlying data powers a free &lt;a href="https://cantoncompliancehub.ch/audit" rel="noopener noreferrer"&gt;compliance self-assessment&lt;/a&gt; that asks a couple of plain questions and returns a personalised overview.&lt;/p&gt;

&lt;p&gt;If you work with open civic or compliance data and want to compare notes on grounding or formats, the datasets and the site are at cantoncompliancehub.ch. Questions welcome in the comments.&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>data</category>
      <category>switzerland</category>
      <category>showdev</category>
    </item>
    <item>
      <title>What I learned building a compliance answer engine that refuses to make things up</title>
      <dc:creator>Adi CCH</dc:creator>
      <pubDate>Tue, 07 Jul 2026 21:53:18 +0000</pubDate>
      <link>https://dev.to/adi_cch/what-i-learned-building-a-compliance-answer-engine-that-refuses-to-make-things-up-42mp</link>
      <guid>https://dev.to/adi_cch/what-i-learned-building-a-compliance-answer-engine-that-refuses-to-make-things-up-42mp</guid>
      <description>&lt;p&gt;Swiss business compliance is a genuinely hard information problem. There is no national minimum wage, VAT rules live at the federal level, social-insurance (AHV) admin runs through 26 cantonal offices, and most of the official guidance is only in German or French. If you are a foreign founder setting up a GmbH, you are stitching answers together from a dozen government PDFs.&lt;/p&gt;

&lt;p&gt;I have been building &lt;a href="https://cantoncompliancehub.ch" rel="noopener noreferrer"&gt;Canton Compliance Hub&lt;/a&gt;, a free multilingual resource that tries to make that legible. The interesting engineering problem was not the content, it was trust: in a domain where a wrong number can cost someone real money, the system has to be citable and it must not hallucinate. Here is what actually worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two-layer retrieval beats one
&lt;/h2&gt;

&lt;p&gt;The naive RAG setup is: chunk documents, embed them, retrieve top-k, stuff into the prompt. It works until a legal term like AHV or MWST needs an exact match that a dense vector glosses over.&lt;/p&gt;

&lt;p&gt;I run two layers on Postgres with pgvector:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raw chunks&lt;/strong&gt; are the ground truth. They keep the original wording and the source URL, so every answer can point back to a specific government page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fact cards&lt;/strong&gt; are distilled atoms extracted from those chunks, embedded separately, and queried first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Retrieval is hybrid: a BM25 pass catches the exact legal acronyms, a vector pass catches the paraphrases, and a metadata filter (canton, topic, language) runs before ranking so a Geneva question never pulls a Zurich-only rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make hallucination structurally hard
&lt;/h2&gt;

&lt;p&gt;"Please don't hallucinate" in a system prompt is not a control. What worked was making every factual claim carry a citation marker back to a chunk, then treating any claim without one as a defect.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generation emits inline citation markers. A claim with no marker gets flagged.&lt;/li&gt;
&lt;li&gt;A cite whose id is not in the retrieved set is a hallucinated citation, and the draft is rejected and retried at temperature 0.&lt;/li&gt;
&lt;li&gt;For the numbers (rates, thresholds, CHF amounts), a separate verification pass marks each numeric claim supported, unsupported, or contradicting against the retrieved evidence only. Unsupported numbers block publication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last pass was the highest-leverage thing we added. Cheap draft models are confident and wrong exactly where it matters most in compliance: a plausible-looking rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change detection is code, not AI
&lt;/h2&gt;

&lt;p&gt;The corpus has to stay current, but re-summarising an unchanged page with an LLM is waste. Change detection is four boring signals: Last-Modified, ETag, an on-page timestamp, and a SHA-256 of the extracted text. The LLM only runs when the content genuinely changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship the primitives as tools
&lt;/h2&gt;

&lt;p&gt;Two byproducts became reusable public artifacts, grounded in official sources and published as open datasets (CC BY 4.0): a minimum-wage-by-canton comparison, and an SME &lt;a href="https://cantoncompliancehub.ch/resources/compliance-calendar" rel="noopener noreferrer"&gt;compliance-deadline calendar&lt;/a&gt;. The whole retrieval stack sits behind a free &lt;a href="https://cantoncompliancehub.ch/audit" rel="noopener noreferrer"&gt;compliance self-check&lt;/a&gt;: describe your situation in plain language and get a personalised, source-cited overview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Two-layer retrieval (atoms first, raw chunks as citation fallback) is worth the extra table.&lt;/li&gt;
&lt;li&gt;Treat a missing or unknown citation as a build error, not a style preference.&lt;/li&gt;
&lt;li&gt;Verify numbers as a separate pass. The draft model will keep inventing them; a checker against your own corpus is the fix.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you work with Swiss compliance content and want to compare notes, the site is &lt;a href="https://cantoncompliancehub.ch" rel="noopener noreferrer"&gt;cantoncompliancehub.ch&lt;/a&gt;. Happy to answer questions in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>rag</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
