Skip to main content
ThunderLang
Draft documentation. Syntax and behavior are illustrative and will change before v1.

Case study: a feature-flag gate, proven across four languages

Every product has a feature-flag evaluator: a kill switch, an allowlist, a percentage rollout. It is small, it is copied between services, and when it drifts (a refactor that forgets the kill switch, an AI edit that logs the override token) the blast radius is everyone. This is a good first mission to adopt ThunderLang on, because the logic is real, the failure is expensive, and the whole loop fits on one page. The runnable files are in examples/adoption/.

1. Bootstrap intent from the code you already have

Start from an existing evaluator and let lift recover a first draft. It is deliberately humble and tells you what a human must still decide:

$ thunder lift flag-eval.before.ts
thunder lift flag-eval.before.ts -> .intent/flagstate.thunder
  [warning] INTENT_LIFT_NEEDS_HUMAN_REVIEW: This intent was inferred from code. A human must review goal, why, never rules, and verification.
  [warning] INTENT_LIFT_NO_TEST_EVIDENCE: No tests found. Guarantees could not be grounded in verification evidence.
  [warning] INTENT_LIFT_SECURITY_REVIEW_NEEDED: Sensitive field names detected. Mark them Secret/Token/PII and add never-log rules.

2. Author the contract

A human reviews the draft and turns it into intent that states what must hold, not just what the code does today:

mission FeatureFlagRollout

goal
  Decide whether a feature flag is on for a given user

input
  flagEnabled: Boolean
  userAllowlisted: Boolean
  userInRolloutBucket: Boolean
  overrideToken: Secret

output
  state: FlagState

guarantee a disabled flag is off for everyone
  because a kill switch that leaks is not a kill switch
  verify test FlagDecision

guarantee an allowlisted user on an enabled flag is on
  because the allowlist is how we dogfood before rollout
  verify test FlagDecision

never log the override token
  because the override token lets anyone force a flag state
  verify test raw override token not logged

decision FlagDecision
  inputs
    flagEnabled
    userAllowlisted
    userInRolloutBucket
  rule killSwitch
    when flagEnabled == false
    return Off
  rule allowlist
    when userAllowlisted == true
    return On
  rule rollout
    when userInRolloutBucket == true
    return On
  default
    return Off

test FlagDecision
  case disabled beats allowlist
    given flagEnabled false, userAllowlisted true, userInRolloutBucket true
    expect Off
  case allowlisted user is on
    given flagEnabled true, userAllowlisted true, userInRolloutBucket false
    expect On
  case rollout bucket is on
    given flagEnabled true, userAllowlisted false, userInRolloutBucket true
    expect On
  case default is off
    given flagEnabled true, userAllowlisted false, userInRolloutBucket false
    expect Off

3. Prove every target agrees

The same test cases run against the decision compiled to each declared language. --all-targets executes every toolchain that is installed and grades the real output:

$ thunder conform FeatureFlagRollout.thunder --all-targets
thunder conform FeatureFlagRollout.thunder: 4 case(s) · semantic + 4 target(s)
                                           Semantic  Typescript  Python    Csharp    Java
  FlagDecision / disabled beats allowlist  PASS      PASS        PASS      PASS      PASS
  FlagDecision / allowlisted user is on    PASS      PASS        PASS      PASS      PASS
  FlagDecision / rollout bucket is on      PASS      PASS        PASS      PASS      PASS
  FlagDecision / default is off            PASS      PASS        PASS      PASS      PASS

The kill switch is not a comment or a code review convention now. It is a rule that TypeScript, Python, C#, and Java are all held to, from one source of truth.

4. Gate the AI's change before it merges

An assistant is asked to add logging. It writes a reasonable-looking line that happens to send the override token to the logs. verify-diff refuses it:

$ thunder verify-diff FeatureFlagRollout.thunder \
    --before flag-eval.before.ts --after flag-eval.after.ts
thunder verify-diff FeatureFlagRollout.thunder vs flag-eval.after.ts: BLOCK (1 blocking, 1 regression(s))
  [VIOLATION] Added code may violate never-rule "log the override token": console.log("evaluating flag", { userAllowlisted, overrideToken });  (line 7)

Exit code 1. No AI ran in that check; the verdict is deterministic. Drop verify-diff into CI or an agent loop (see Verifying AI code changes and ThunderLang for AI agents) and the change cannot merge until the token stops leaking.

The point

The evaluator was never rewritten. ThunderLang wrapped the decision the team already had in a contract, proved four language targets agree on it, and turned "do not log the token" from a hope into a gate. That is adoption without a migration: lift what exists, state what must hold, and let the tooling hold the line.