Towards Repeatable Compliance: The Basics

Defining auditable validation frameworks on top of your data

Defining auditable validation frameworks on top of your data
Content

Problem statement

Most teams store requirements as text scattered across Jira, Confluence and Slack. Every notation invented for them — MoSCoW, EARS, Gherkin — makes a requirement clearer. None makes one checkable: no Jira board can say whether the rule it describes held last March, or which of its own rules has never been tested.

That mattered less when a human read every ticket before writing code. It matters more now that agents do much of that reading: as I argued in The Pull Request Future in the Agentic Era, the leverage is moving upstream, to the requirement itself.

Code is becoming a commodity. Leverage is moving upstream, to the requirements layer.

But compliance is not visible in the data: the requirements have to exist somewhere as a thing that runs.

In this post we take two ordinary business requirements from financial services as example and capture them using three different requirement frameworks by converting requirements into controls, then controls into auditable stored procedures that run repeatable verifications on demand.

Pipeline diagram: requirements in plain text become controls through an LLM, the controls become a SQL procedure through a second LLM, a subject-matter expert reviews the procedure and either approves it or sends it back, the approved procedure is stored in the database, and executing it marks each requirement verified or not verified.
Figure 1. From a written requirement to a recorded verdict.

Three notations, three audiences

Let’s take MoSCoW, EARS, and Gherkin requirement frameworks.

Sometimes they get taught as competing “best practices,” but they’re not competing. They answer three different questions, for three different people, and a healthy requirement usually passes through all three.

NotationAnswersPrimary audienceShape
MoSCoWShould we build it, and how badly do we need it?Product owners, sponsors, anyone negotiating scopeA priority label on a backlog item
EARSExactly what must the system do?Requirements and systems engineers, especially safety-criticalA constrained natural-language sentence
GherkinHow do we know it’s done?QA, developers, analystsAn executable scenario

MoSCoW — Decide what ships

MoSCoW came out of Oracle UK, authored by Dai Clegg in 1994. It classifies requirements in Must, Should, Could, Won’t have this time.

It says nothing about how a requirement is satisfied. Its job is forcing a room to admit that not everything is a Must, before a deadline forces the same admission under worse conditions — a business conversation, not an engineering one.

Won’t is not the absence of a decision but a decision recorded and dated.

EARS — Build it exactly right

Freeform prose fails predictably: “the system shall respond quickly” passes review, then means five different things to five engineers. The Easy Approach to Requirements Syntax (EARS), developed by Mavin, Wilkinson, Harwood, and Novak at Rolls-Royce fixes that by constraining the grammar rather than the vocabulary.

Five templates cover almost every requirement shape:

  • Event-driven (WHEN)
  • State-driven (WHILE),
  • Optional feature (WHERE)
  • Unwanted behaviour (IF/THEN)

They do compose, so the fullest form of the sentence reads:

WHILE <precondition>, WHEN <trigger>, the <system> SHALL <response>.

Nothing written this way is ambiguous, and that’s the point: EARS is aimed at the person who has to implement and test the sentence, not the stakeholder deciding whether it’s worth building.

Gherkin — Proving that it works

Gherkin takes a different starting point: instead of constraining a single sentence, it structures a worked example.

Given/When/Then reads as prose but parses as a test. Here it is on the first of the two rules below. It names no row and no date: the scenario states the behaviour, and binding it to data is the procedure’s job.

Scenario: The initiator cannot release their own transfer
  Given a transfer initiated by an employee
  When that same employee submits the release
  Then the release is refused and the transfer is not transmitted

Where EARS produces a sentence an engineer implements, Gherkin produces a scenario a test suite runs.

Notice that it is also the only one of the three that produces evidence.

Whether something is specified well and whether it actually worked are different questions, and a dated scenario run on every build answers the second in a way an EARS sentence never can.

The three notations are not three views of one thing: MoSCoW establishes scope, EARS establishes design, and Gherkin establishes evidence. A requirement written in only one of them is weak in the other two.

A requirement written in only one of them is weak in the other two.

Two use cases

Let’s use two simple, unglamorous use cases that are related to controls defined in financial services.

The following examples can be distilled from the COSO framework as part of operational controls to align with SOX regulation.

The following are two rules that almost every company above a certain size has written down somewhere, whichever regulator happens to be asking:

  • Maker-checker validation. Whoever initiates a certain order cannot be the person who approves it — and if they try, the approval is refused rather than logged and allowed. This is the REQ-1xx block below.
  • Mandatory leave. Anyone holding a decision-making role takes at least 20 consecutive calendar days of leave each year, on the theory that most concealment needs daily maintenance and will not survive three uninterrupted weeks of somebody else doing the job. This is the REQ-2xx block.

The database schema

Let’s define a database schema to support our examples.

  • Requirement holds one row per rule in plain language, with an importance column carrying its MoSCoW priority and a validationProcedure naming the one procedure that runs all of that rule’s controls at once.
  • Control holds the controls over it, one per EARS sentence or Gherkin scenario.
  • StoredProcedures points the other way: each procedure carries the controlId of the control it checks, so a control that nothing checks is simply a control nothing points at.
  • ControlResult has a dated line for every time a check ran and what it found.

Cilla Ugarte and colleagues open their Compliance-as-Code architecture on something very similar — governance frameworks “specify what to assure but provide no executable format for how” — and answer it by emitting evidence as a byproduct of the work being governed.

On the operational data side we would find the enterprise data repository.

The following diagram brings the whole schema together; notice how the requirements tables are separated from the operational data being evaluated.

Schema diagram: the requirements register on the left, the stored-procedure catalog in the middle, and the operational tables on the right.
Figure 2. The requirements register and the operational data it governs, linked only through the procedures catalog.

The operational controls are defined as immutable stored procedures, which means they can be recreated with infrastructure-as-code (IaaC) and audited for modifications.

This is the part where AI agents come to scene, creating accurate SQL queries out of requirements and data schemas.

AI Agents implement Text-to-Sql, generating the procedures, but a human validates them.

Stored procedures can be drafted by LLMs through natural-language-to-SQL (text-to-SQL), but review stays human. What accumulates is a library of pre-approved functions, each holding SQL an expert has checked — moving expert attention to validation rather than creation.
This is critical in highly-risk scenarios (check de Costa et al, 2025 for a concrete example).

An LLM can propose the procedure, an expert approves it, and what the register points at afterwards is an immutable check in the form of a SQL query.

Our test data looks like this:

emp_idnamejob_roledecision_maker
E-01DANATreasury Managertrue
E-02RAVIPayments Officerfalse
E-03MEIFinance Directortrue
transfer_idinitiated_byinitiated_atreleased_byreleased_at
WT-2001250,000.00E-012026-03-02E-02
WT-200288,000.00E-032026-03-04E-03
employeeIdstartDateendDate
E-012026-07-062026-07-27
E-032026-02-022026-02-06
E-032026-05-042026-05-08
E-032026-08-032026-08-07
E-032026-11-022026-11-06

WT-2002 was initiated and released by the same person. Mei’s leave adds up to twenty days — the number the rule asks for — but across four separate weeks, the longest five days. Dana took twenty-two consecutive days and is compliant.

Use case 1: maker-checker, in three notations

MoSCoW produces no procedure of its own but it flags the importance of the requirement.

Requirement — the maker-checker rule, in plain text

reqIdimportancevalidationProceduredescription
REQ-102Mustvalidate_req_102A transfer is released by an identity different from the one that initiated it.

EARS and Gherkin: each one produces a control over the requirement, with its own id and its own statement of how the requirement is pinned down.

Control — the controls over REQ-102 (the procedure each one runs comes later)

controlIdmethodologydescription
REQ-102-C1EARSIF the releasing identity is the same as the initiating identity, THEN the Payment Service SHALL refuse the release and SHALL NOT transmit the payment.
REQ-102-C2GherkinScenario: The initiator cannot release their own transfer
Given a transfer initiated by an employee
When that same employee submits the release
Then the release is refused and the transfer is not transmitted
REQ-102-C4EARSWHEN a transfer is released, the Payment Service SHALL record both the releasing identity and the release timestamp.
REQ-102-C5GherkinScenario: Every release names who released it and when
Given a transfer that has been released
When its release record is read
Then it carries both a releasing identity and a release timestamp

Five controls over one requirement: three EARS sentences and two Gherkin scenarios, using three of the five EARS templates.

EARS → an invariant over the business, stored as a procedure. The translation is close to mechanical: the subject names the table, the precondition becomes the WHERE, and the SHALL NOT is the thing we want to detect.

Show SQL
CREATE MACRO check_req_102() AS TABLE (
  SELECT t.transfer_id, t.amount_eur, e.name AS initiated_and_released_by
    FROM Transfer t
    JOIN TransferRelease r ON r.transfer_id = t.transfer_id
    JOIN Employee e         ON e.emp_id      = t.initiated_by
   WHERE r.released_by = t.initiated_by
);

Gherkin → an assertion over named rows, carrying its own expected answer. The Given pins the data, the Then pins the verdict, so the procedure returns a pass or a fail per scenario rather than a list of offenders to review. Each case names a transfer and expects it to be compliant; the verdict comes from the rule itself, comparing the releasing identity to the initiating one:

Show SQL
CREATE MACRO check_scenario_102() AS TABLE (
  WITH scenario(name, given_transfer, expected) AS (
    VALUES ('initiator-cannot-release-own-transfer',   'WT-2002', 'compliant'),
           ('a-second-identity-releases-the-transfer', 'WT-2001', 'compliant')
  )
  SELECT s.name AS scenario, s.expected,
         CASE WHEN r.released_by = t.initiated_by
              THEN 'non-compliant' ELSE 'compliant' END AS actual,
         CASE WHEN (CASE WHEN r.released_by = t.initiated_by
                         THEN 'non-compliant' ELSE 'compliant' END)
                   = s.expected THEN 'pass' ELSE 'FAIL' END AS result
    FROM scenario s
    JOIN Transfer t             ON t.transfer_id = s.given_transfer
    LEFT JOIN TransferRelease r ON r.transfer_id = s.given_transfer
);

Neither query could stand in for the other: EARS tells you what the business did, Gherkin is the only one that knows what should have happened.

Use case 2: mandatory leave, in three notations

Same treatment: one requirement, one plain sentence, one id.

reqIdimportancevalidationProceduredescription
REQ-201Mustvalidate_req_201An employee in a designated decision-making role takes at least 20 consecutive calendar days of leave each year.

And four controls over it.

controlIdmethodologydescription
REQ-201-C1EARSThe Workforce Service SHALL record at least 20 consecutive calendar days of leave per calendar year for every employee in a designated decision-making role.
REQ-201-C2GherkinScenario: Twenty days must be consecutive, not cumulative
Given an employee in a decision-making role whose leave is split into several short periods
When the mandatory leave check runs for the year
Then that employee is reported as non-compliant
REQ-201-C4EARSThe Workforce Service SHALL record leave for every employee in a designated decision-making role.
REQ-201-C5GherkinScenario: Every decision-making role has leave on record
Given an employee in a designated decision-making role
When the mandatory leave check runs for the year
Then at least one leave period is recorded for that employee

REQ-201-C4 is an ubiquitous template — no trigger, true of everyone in scope at all times — and the cheapest control here: an employee who took no leave never appears on a leave-balance exception report, having no balance to be exceptional about.

Show SQL
-- periods clipped to the year, then merged into blocks
CREATE MACRO leave_blocks(yr) AS TABLE (
  WITH clipped AS (
    SELECT employeeId,
           greatest(startDate, make_date(yr,  1,  1)) AS startDate,
           least(   endDate,   make_date(yr, 12, 31)) AS endDate
      FROM Leaves
     WHERE startDate <= make_date(yr, 12, 31)
       AND endDate   >= make_date(yr,  1,  1)),
  ordered AS (
    SELECT employeeId, startDate, endDate,
           max(endDate) OVER (PARTITION BY employeeId ORDER BY startDate
                              ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS prevEnd
      FROM clipped),
  blocks AS (
    SELECT employeeId, startDate, endDate,
           sum(CASE WHEN prevEnd IS NULL OR startDate > prevEnd + 1 THEN 1 ELSE 0 END)
             OVER (PARTITION BY employeeId ORDER BY startDate) AS blockId
      FROM ordered)
  SELECT employeeId, datediff('day', min(startDate), max(endDate)) + 1 AS days
    FROM blocks GROUP BY employeeId, blockId
);

CREATE MACRO check_req_201(yr) AS TABLE (
  SELECT e.name, e.job_role,
         coalesce(sum(b.days), 0) AS total_leave_days,
         coalesce(max(b.days), 0) AS longest_consecutive_block
    FROM Employee e
    LEFT JOIN leave_blocks(yr) b ON b.employeeId = e.emp_id
   WHERE e.decision_maker
   GROUP BY e.name, e.job_role
  HAVING coalesce(max(b.days), 0) < 20
);

Both columns are in the output on purpose. One of them is what a leave-balance report shows; the other is what the requirement actually asked for.

Gherkin follows the same shape as check_scenario_102(), pinning each scenario to a named employee and an expected verdict, over the same leave_blocks(yr).

Ten SQL controls now exist for two business rules, and they divide cleanly by notation rather than by use case:

NotationRuns againstThe SQL isA clean run returns
MoSCoWnothing — it is a column on the requirementnot a query at all—
EARSoperational dataan invariant scannothing at all
Gherkinnamed rows plus an expected answeran assertionpass, per scenario

Running them

Everything below is reproducible. The schema, the sample rows, every procedure and the queries that follow live in one script, requirements-validation.sql, which prints these result tables in the order this section shows them. Beside it, experiments.sql perturbs the data so the checks can be seen reacting rather than only passing.

Eight controls exist across the two requirements, and each names a stored procedure. That is what the register asserts. Whether those procedures exist is a separate question, and no document can answer it. Ask the catalog:

Show SQL
SELECT sp.controlId, sp.procedureName,
       CASE WHEN f.function_name IS NULL THEN 'MISSING' ELSE 'ok' END AS in_catalog
  FROM StoredProcedures sp
  LEFT JOIN duckdb_functions() f
         ON f.function_name = sp.procedureName
        AND f.function_type = 'table_macro'
 ORDER BY sp.controlId;
controlIdprocedureNamein_catalog
REQ-102-C1check_req_102ok
REQ-102-C2check_scenario_102ok
REQ-102-C4check_req_102_attributionok
REQ-102-C5check_scenario_102_attributionok
REQ-201-C1check_req_201ok
REQ-201-C2check_scenario_201ok
REQ-201-C4check_req_201_any_leaveok
REQ-201-C5check_scenario_201_any_leaveok

Here the two agree. The value is that they can be compared at all: the register asserts a name, the catalog holds what is callable, and reconciling them costs one LEFT JOIN. Stored as text in a column, an unwritten check is indistinguishable from a written one.

Use case 1

EARS first, SELECT * FROM check_req_102():

transfer_idamount_eurinitiated_and_released_by
WT-200288000.00MEI

Not empty, so not a pass. Mei initiated an 88,000 EUR transfer and released it herself.

And Gherkin, SELECT * FROM check_scenario_102():

scenarioexpectedactualresult
a-second-identity-releases-the-transfercompliantcompliantpass
initiator-cannot-release-own-transfercompliantnon-compliantFAIL

The same fact, reported differently, and the difference is the point: EARS hands back a row to interpret, Gherkin hands back the word FAIL next to the name of the thing that was supposed to happen.

Use case 2

REQ-201 is a Must, and all four of its controls run. Three pass. The one that fails is the only one measuring what the requirement specifies.

EARS, SELECT * FROM check_req_201(2026):

namejob_roletotal_leave_dayslongest_consecutive_block
MEIFinance Director205

Those two numbers are the whole post in one row. Twenty and five, same person, same year: total_leave_days is what the HR report shows, longest_consecutive_block is what the requirement actually asked for. Only one of them is a control.

Gherkin, SELECT * FROM check_scenario_201(2026):

scenarioexpectedactualresult
one-uninterrupted-block-satisfiescompliantcompliantpass
twenty-days-must-be-consecutivenon-compliantnon-compliantpass

Both pass — while Mei sits in violation one query above. Not a contradiction: the Gherkin scenarios assert that the check behaves as specified.

Validating a requirement

Running controls one at a time answers one question at a time. Each requirement also names a single procedure — validate_req_ plus its id — that runs every control it has and returns a verdict for each:

Show SQL
CREATE MACRO validate_req_102() AS TABLE (
              SELECT 'REQ-102-C1' AS controlId, 'EARS' AS methodology,
                     CASE WHEN (SELECT count(*) FROM check_req_102()) = 0
                          THEN 'pass' ELSE 'FAIL' END AS result
    UNION ALL SELECT 'REQ-102-C2', 'Gherkin',
                     CASE WHEN (SELECT count(*) FROM check_scenario_102() WHERE result = 'FAIL') = 0
                          THEN 'pass' ELSE 'FAIL' END
    UNION ALL SELECT 'REQ-102-C4', 'EARS',
                     CASE WHEN (SELECT count(*) FROM check_req_102_attribution()) = 0
                          THEN 'pass' ELSE 'FAIL' END
    UNION ALL SELECT 'REQ-102-C5', 'Gherkin',
                     CASE WHEN (SELECT count(*) FROM check_scenario_102_attribution() WHERE result = 'FAIL') = 0
                          THEN 'pass' ELSE 'FAIL' END
);

The two notations reduce to one verdict each in different ways: an EARS control passes when its invariant returns nothing, a Gherkin control passes when no scenario came back FAIL. SELECT * FROM validate_req_102():

controlIdmethodologyresult
REQ-102-C1EARSFAIL
REQ-102-C2GherkinFAIL
REQ-102-C4EARSpass
REQ-102-C5Gherkinpass

And validate_req_201(2026):

controlIdmethodologyresult
REQ-201-C1EARSFAIL
REQ-201-C2Gherkinpass
REQ-201-C4EARSpass
REQ-201-C5Gherkinpass

Neither procedure reports a clean requirement. Every control ran and returned a verdict — a statement supported by the run itself rather than by the document.

Recording what happened

A run that leaves no trace proves nothing, so each run writes a line. A few of the ten:

timestampreqIdcontrolIdresultdescription
2026-03-31 02:00REQ-102REQ-102-C1falseWT-2002 (88,000.00 EUR) initiated and released by MEI
2026-04-30 02:00REQ-102REQ-102-C1trueNo transfer released by its own initiator
2026-12-31 02:00REQ-102REQ-102-C1falseWT-2002 (88,000.00 EUR) initiated and released by MEI
2026-12-31 02:00REQ-102REQ-102-C2falseinitiator-cannot-release-own-transfer: expected compliant, got non-compliant
2026-12-31 02:00REQ-201REQ-201-C1falseMEI: 20 leave days in 2026, longest unbroken block 5
2026-12-31 02:00REQ-201REQ-201-C2trueBoth scenarios behaved as specified
2026-12-31 02:00REQ-102REQ-102-C4trueEvery release carries an identity and a timestamp

result is the verdict and description is what the procedure saw — false alone is an alert, while “false, WT-2002, 88,000 EUR, MEI” is something a person can act on without rerunning anything.

Validating all the requirements

Show SQL
WITH latest AS (
  SELECT controlId, arg_max(result, timestamp) AS result, max(timestamp) AS ts
    FROM ControlResult GROUP BY controlId
)
SELECT r.reqId, r.importance,
       count(DISTINCT c.controlId)  AS controls,
       count(DISTINCT sp.controlId) AS with_procedure,
       coalesce(CAST(max(l.ts) AS VARCHAR), '(never run)') AS last_run,
       CASE WHEN count(DISTINCT l.controlId) = 0 THEN '(no evidence)'
            WHEN NOT bool_and(l.result)            THEN 'FAIL'
            WHEN count(DISTINCT l.controlId)
                 < count(DISTINCT c.controlId)     THEN 'incomplete'
            ELSE 'pass' END AS verdict
  FROM Requirement r
  LEFT JOIN Control c            ON c.reqId = r.reqId
  LEFT JOIN StoredProcedures sp  ON sp.controlId = c.controlId
  LEFT JOIN latest l             ON l.controlId = c.controlId
 GROUP BY r.reqId, r.importance
 ORDER BY r.reqId;
reqIdimportancecontrolswith_procedurelast_runverdict
REQ-102Must442026-12-31 02:00:00FAIL
REQ-201Must442026-12-31 02:00:00FAIL

Two Musts, both failing right now, dated and with the detail attached. REQ-102 fails with two of four controls passing — a requirement is only as true as its weakest control.

4 Conclusions

The three notations are not interchangeable. MoSCoW compiles to nothing at all, importance is an attribute. EARS compiles to an invariant where an empty result is the pass. Gherkin compiles to an assertion carrying its own expected answer.

Semantics in the sentence is what makes the query useful. check_req_201 is a handful of lines, and every one was decided by a single word. Because the requirement said consecutive, the SQL could not be sum(days), and the gap between 20 and 5 became visible. “Twenty days of leave a year” would have been easier to write, correct against the specification, and worthless.

LLMs simplify converting requirements to SQL code. Text-to-SQL makes the translation cheap, which moves the cost onto the two things it does not do: writing a sentence precise enough to translate, and reviewing what comes back. The procedures here are small enough to read in full, which is what makes that review possible.

It´s repeatable and auditable. Seven tables, four procedures, a handful of queries, no infrastructure. A requirement written well enough is already most of a query.

Resources

Klein, T., & Hoffart, J. (2026). Statistically indistinguishable, operationally distinct: A formal barrier for tabular foundation models. arXiv. https://arxiv.org/abs/2606.29091

de Costa, M., Anwar, M., Mercier, D., Randall, M., & Hammad, I. (2025). Enhancing accuracy and maintainability in nuclear plant data retrieval: A function-calling LLM approach over NL-to-SQL. arXiv. https://arxiv.org/abs/2506.08757

Cilla Ugarte, R., Patricio Guisado, M. Á., Berlanga de Jesús, A., & Molina López, J. M. (2026). Making AI compliance evidence machine-readable. arXiv. https://arxiv.org/abs/2604.13767

Mavin, A., Wilkinson, P., Harwood, A., & Novak, M. (2009). Easy approach to requirements syntax (EARS). 2009 17th IEEE International Requirements Engineering Conference (RE'09), 317–322. https://doi.org/10.1109/RE.2009.9

Clegg, D., & Barker, R. (1994). CASE method fast-track: A RAD approach. Addison-Wesley.

Agile Business Consortium. (n.d.). MoSCoW prioritisation. https://www.agilebusiness.org/dsdm-project-framework/moscow-prioritisation.html

Cucumber. (n.d.). Gherkin reference. https://cucumber.io/docs/gherkin/reference/

National Institute of Standards and Technology. (n.d.). Open Security Controls Assessment Language (OSCAL). https://pages.nist.gov/OSCAL/

Committee of Sponsoring Organizations of the Treadway Commission. (n.d.). Internal control — Integrated framework. https://www.coso.org/guidance-on-ic

Sarbanes-Oxley Act of 2002, Pub. L. No. 107-204, 116 Stat. 745. https://www.govinfo.gov/content/pkg/PLAW-107publ204/pdf/PLAW-107publ204.pdf

Easy approach to requirements syntax. (n.d.). In Wikipedia. https://en.wikipedia.org/wiki/Easy_Approach_to_Requirements_Syntax