I was sitting in front of the GitHub shell, looking at the audit report on my own 17 public repos. The scanner had run. Clojure had parsed every markdown file. The verdicts were in.

I turned to Buffy and asked the question that should always come before any compliance review — the one-word version:

“give 1 word compliance or not”

The answer was non-compliant.

It was technically true. There were gaps in my planning should any of those repos ever go live ingesting real personal data. But it was the wrong kind of true. The word implied a moral failing where only an engineering choice was missing.

I asked the next question — the one that separates “audit” from “anxiety”:

“so we are 100% compliant?”

The answer was yes — for the display-only state the repos actually lived in. No real users, no live services, no personal data flowing. PDPA does not apply until someone starts collecting, using, or disclosing personal data in Singapore.

That gap — the gap between “engineered but not deployed” and “actually handling users” — is where today’s tooling fails. Compliance is handled by policy PDFs that nobody reads at code review time. Checklist spreadsheets that drift from the codebase as it evolves. Consultants charging five figures to produce a document that is stale by Friday.

What if compliance were a read-through artifact instead? A single repo you point an AI agent at, where the agent reads top-to-bottom, ticks the right boxes, and walks away compliant — for the 11 PDPA obligations that haven’t changed materially since 2012.

That repo shipped yesterday. It’s called pdpa-sg-clj, and this post is its autobiography.


1. Code as Policy: Architecture and the Auto-Tick Pattern

Why Compliance-as-Code, Not Compliance-as-Policy

Singapore’s Personal Data Protection Act (2012, last revised 2026) puts 11 obligations on every organisation that processes personal data. They don’t change often. They aren’t subjective. The PDPC publishes them plainly. And yet organisations routinely fail compliance audits — not because they don’t know the rules, but because the rules live in documents that the code does not enforce.

The same 11 obligations, encoded so a build-step can read them:

Architecture Diagram

Note: The 3-day breach notification clock starts at the assessment of notifiability (not discovery), governed by an “as soon as practicable” floor. The 31 Dec 2026 deadline phases out NRIC for authentication (logins/passwords), distinct from the older 2018 display-masking rules.

The toolkit’s job is to make each of those 11 obligations a runnable check. Some — the scanner obligations like “no hardcoded secrets” — can be performed by code. Some — the documentation obligations like “publish a Privacy Policy” — must be performed by humans but can be autoticked when the human publishes the proof file.

That bifurcation is the central architectural idea. Anything a scanner can verify gets a <!-- agent:verify-X --> marker in CHECKLIST.md, and bb audit ticks it for you. Anything only humans can verify stays manual. The scanner does the boring work; the human does the judgement work.


Repository Anatomy

The repo lives at nurazhardotcom/pdpa-sg-clj. Here’s how it’s organised:

Architecture Diagram

The architecture is intentionally flat. Each namespace has one responsibility and a name that matches its noun. A call-graph walks from pdpa.core straight down through pdpa.redact → pdpa.nric, pdpa.scan → pdpa.nric, pdpa.audit → pdpa.scan + pdpa.checklist, and so on.

The Babashka CLI surface maps one task to one namespace:

Architecture Diagram

Every task uses (requiring-resolve 'pdpa.x/y) so each invocation loads only the namespace it needs. Startup time on cold bb is sub-second.


The CHECKLIST.md Pattern

CHECKLIST.md is the heart of the toolkit. It’s a single Markdown file with - [ ] boxes for each obligation. Boxes that the scanner can verify have an HTML comment marker, like <!-- agent:verify-protection -->. The auto-tick logic reads both the file and the scan results in one pass:

Architecture Diagram

Here’s a snippet from CHECKLIST.md to make it concrete:

### Protection Obligation

- [ ] **Encryption at rest** for personal-data fields (AES-256 or equivalent)
- [ ] **Encryption in transit** (TLS ≥ 1.2)
- [ ] Access control (RBAC) with quarterly access-list review
<!-- agent:verify-protection -->
- [ ] **No hardcoded secrets** in tracked files
- [ ] **No raw NRICs** in code — `pdpa.redact` strips them

### Accountability / DPO Designation Obligation

- [ ] **DPO named** with full name + business email — see `policies/DPO_CONTACT.md`
<!-- agent:verify-dpo -->
- [ ] DPO contact published on homepage footer

The scanner guarantees canonical protection when: - 0 CRITICAL NRIC leaks in code - 0 HIGH severity credential leaks
- A file named SECURITY_HARDENING.md exists in the project root

When all three are true, the box under <!-- agent:verify-protection --> flips to [x]. The other four boxes in that section are still manual, because encryption design and RBAC audit policy aren’t things a scanner can determine from a file walk.

The split — what a scanner can verify vs what only humans can verify — is the design discipline.


2. The Technical Deep-Dive: Validation, Redaction, and Audit

The Mod-11 Algorithm and False Positives

Singapore NRIC validation is one of those algorithms that looks trivial but has subtle landmines. The structural regex [STFG]\d{7}[A-Z] matches roughly 1 in every 36M random 9-character windows — including BSV transaction IDs, SHA-256 chunks, and git short hashes. Without a check-digit filter, a structural-match redactor would scream CRITICAL on every repo that touches blockchain code.

The check-digit algorithm:

Architecture Diagram

Hand-computed reference values used by the test suite:

Input Computation Output
S0100000J sum=1×7=7, (7+4)%11=0 → “JZIHGFEDCBA”[0] ✓ valid
F0000002K sum=2×2=4, (4+4)%11=8 → “XWUTRQPNKLM”[8] ✓ valid
M5000000P sum=3×1+5×2=13, (13+4)%11=6 → “XWUTRQPNKLM”[6] ✓ valid
S0000000Z sum=0, (0+4)%11=4 → “JZIHGFEDCBA”[4]=‘G’ ✗ rejected
deadbeef…F hex — sum is wild, never valid ✗ rejected

Why this matters in practice: my BSV transaction-hash-redacting work would have been wrecked by a structural-only regex. Every txid that contains 7 consecutive digits followed by a letter (S/N/T/F/G are common hex letters) would have been redacted as NRIC. The Mod-11 filter reduces that from 1-in-36M to something astronomically lower — only real NRICs pass.


The Redaction Pipeline

The redactor runs four stages in fixed order on any text body:

Architecture Diagram

Idempotency is verified in tests: calling redact-text on the redacted output returns the same string. The redactor is safe to run in a CI pre-commit hook; running it twice doesn’t double-mangle.

A worked example:

(require '[pdpa.redact :as r])

(r/redact-text
  "User S0100000J contacted us at +65 9123 4567 or alice@acme.com.sg")
;; => {:redacted
;;     "User [REDACTED_NRIC] contacted us at [REDACTED_PHONE] or [REDACTED_EMAIL]"
;;     :counts {:nric 1 :phone 1 :email 1}
;;     :warnings []}

Orchestrating the Audit

When the user demands a single verdict — is this project compliant?bb audit ./your-project answers in one shot:

Architecture Diagram

The write is the part I’m proudest of. Auto-tick should be invisible when nothing changed. The check (when (and (.exists chk-file) (not= ticked-md chk-md)) (spit chk-file ticked-md)) means a clean bb audit produces zero diff in git — your working tree stays pristine.


3. State and Exposure: Moving to Executable Compliance

The Before and After Shift

The question your reviewer will actually ask is what does the toolkit do that your existing repo cannot? Let me show the two states:

Architecture Diagram

A graph view of the same shift:

Architecture Diagram

The shift is from “documents that describe the policy” to “code that executes the policy.” The PDF never had a chance.


Compliance Exposure Tiers

When I scanned my own 17 public GitHub repos for PDPA-relevant personal data, they fell into five tiers:

Architecture Diagram

The seven repos in the HIGH + HISTORICAL tier are the ones that, if their authors ever flipped them to live-deployment with real users, would need every CHECKLIST.md box ticked. The toolkit gets them from “design draft” to “production-ready” in roughly 30 minutes of work plus policy-text authoring.


Mapping the 11 Obligations

Here’s the explicit mapping from each PDPA obligation to what the toolkit can verify:

# Obligation Auto-Tick Verifier Human Action Required
1 Consent CONSENT_FORM.md exists at project root Write the consent clauses
2 Purpose Limitation 0 MEDIUM-leak findings in scan Field-by-field purpose documentation
3 Notification PRIVACY_POLICY.md exists at project root Fill in 27 <<ORG_NAME>> placeholders
4 Accuracy (manual only) Add validation middleware
5 Protection 0 CRITICAL + 0 HIGH + SECURITY_HARDENING.md exists Implement encryption/RBAC
6 Retention Limitation RETENTION_SCHEDULE.md exists Build the auto-purge job
7 Transfer Limitation (§26) (manual only) Execute an APEC CBPR / PRP / SCC contract
8 Access & Correction (DSR) (manual only) Build /api/dsr/* endpoints
9 Withdrawal of Consent (manual only) Build /api/consent/withdraw
10 Data Breach Notification BREACH_PLAN.md exists Write escalation tree (3-day clock from notifiability assessment)
11 Accountability / DPO DPO_CONTACT.md exists Publish privacy@ on homepage

Five of the eleven have full or partial auto-tick. The other six are unambiguous “human action required” rows in the checklist — the toolkit marks them and refuses to lie about them.


The Event State Machine

Each obligation in CHECKLIST.md has a status that evolves:

Architecture Diagram

That’s where the toolkit stops: at OK, yes, but not at [*]. The final deploy is a human decision involving PDPC, SOC2 audit, regulatory filings — none of which the toolkit can do for you.


4. Shipping v0.1.0: Usage, Scope, and Gaps

What’s in the Box

The repo as of today (21 June 2026):

Component File Lines Purpose
NRIC Mod-11 algorithm src/pdpa/nric.clj ~110 The hex-false-positive-guarded check-digit core
Redaction pipeline src/pdpa/redact.clj ~95 NRIC + phone + email pipeline with idempotency
Scanner + severity classifier src/pdpa/scan.clj ~150 ripgrep wrapper, classify CRITICAL/HIGH/MEDIUM/LOW
Checklist auto-ticker src/pdpa/checklist.clj ~140 CHECKLIST.md parser + verifier-fn map
Policy template loader src/pdpa/policy.clj ~55 <<ORG_NAME>> substitution + custom output path
Audit orchestrator src/pdpa/audit.clj ~110 scan + evidence + auto-tick + JSON-or-human report
Init copier src/pdpa/init.clj ~50 Copies 7 templates + CHECKLIST + README + ARCHITECTURE to target
Rule version stamp src/pdpa/version.clj ~10 Forces a single source of pdpa-sg-clj 0.1.0 / Singapore PDPA 2026-06-21
Public core API src/pdpa/core.clj ~40 Re-exported public surface
Tests test/pdpa/*_test.clj ~140 Mod-11 valid/invalid fixtures, redaction idempotency, auto-tick logic
Policy templates resources/policies/*.template.md 8 files Privacy, DPO, Consent, Breach, Transfer, DPIA, Security, Retention
Master checklist CHECKLIST.md ~140 The 11 obligations with auto-tick markers
Architecture doc ARCHITECTURE.md ~110 Why-things-are-the-way-they-are for contributors

28 files, ~1300 lines of Clojure + ~700 lines of Markdown. Two runtimes — Babashka for the CLI, JVM Clojure for the library consumer.


Using It On Your Own Project

If you have a project that handles personal data:

# 1. Get the toolkit
git clone https://gitlab.com/nurazhar/pdpa-sg-clj

# 2. Drop the checklist + 7 templates into your project
cd pdpa-sg-clj
bb init ../your-project/

# 3. Add pdpa-sg-clj as a dependency in your deps.edn
# (or as a git submodule, depending on your taste)

# 4. Run the audit on your codebase
bb audit ../your-project

# 5. Tick the manual boxes as you ship DPO contact,
#    privacy policy, breach plan, etc.
$EDITOR ../your-project/PDPA_CHECKLIST.md

The whole pipeline runs in under 30 seconds for a typical mid-sized project. The auto-ticked boxes flip immediately. The manual boxes stay unchecked until you ship the proof file.


What’s Still Pending

Honest accounting. The toolkit has gaps:

# Gap Direction
1 clojure -X:test JVM path still fails on Windows because Babashka-only babashka.process is gated by try/catch but the wrapping isn’t perfect Split into pdpa.scan.bb / pdpa.scan.jvm sub-namespaces
2 No pre-commit hook template — currently you wire it manually Ship .git/hooks/pre-commit template + .pre-commit-config.yaml
3 No CI workflow example — you’d need to build your own .github/workflows/pdpa.yml Scaffold the YAML with bb test + bb audit calls
4 The 11 obligations are enforced against English policy text — multilingual orgs will need translation workflows Add a language-aware template generator
5 Zero PDF generation tooling — generated privacy policy is bare markdown Add a bb policy --pdf task using a Clojure-native markdown→PDF lib
6 No SBOM / dependency-list verification (third-party processors covered by §26 are not enumerated) Add pdpa.scan/deps that parses deps.edn + plugin manifests
7 No DPPC API integration — breach notifications still have to be hand-filed at https://www.pdpc.gov.sg (When DPPC exposes an API. Until then: copy/paste the JSON into the portal.)

If any of these are blockers for your specific deployment, file an issue on the GitHub repo. PRs welcome.


5. Why This Matters

Compliance tooling has two failure modes:

  1. Too rigid — it claims compliance and lies when the underlying code changes.
  2. Too loose — it lists every checkbox and drowns the operator in noise.

The way out is to make compliance read-through code. A single Markdown file where an AI agent — or a junior engineer at midnight before the deploy — can read top-to-bottom and tick the right boxes. The right combination is: scanner-verifiable items get auto-ticked; human-judgement items stay manual; the file itself is the artifact.

That’s pdpa-sg-clj. A repo, a checklist, a scanner, a redactor, six templates, and a single line of CLI:

bb audit ./your-project

Throw it at any private project you’re about to push to a Singapore user. If you’re already at the bar, you’ll see ✅. If you’re short of it, you’ll see exactly which boxes still need a human.


If you ship something on top — a bb audit integration, a new policy template, a Mermaid diagram for an obligation — open a PR. Compliance-by-read should be a public conversation, not a paid service.

Sources & Citations


This post and the toolkit are independent projects. The toolkit is MIT-licensed open source; this post is licensed CC BY-NC-SA 4.0. Nothing here is legal advice. For an actual PDPA filing or breach response, hire a Singapore-licensed data-protection counsel. The 3-day breach rule is unforgiving; automation helps you detect, but the call still goes through a human.