Get started

Quickstart

This assumes ulang-lite is vendored at vendor/ulang-lite, with the ./ulang-lite wrapper from Install.

1. Write a contract

Save this as policy/deploy.ul. It decides two actions, deploy and rollback, from six facts about a deploy request.

policy/deploy.ulexample project
// Production deploys: who may ship, and when.
CONTRACT ProductionDeploy {
  ENFORCEMENT_MODE: enforced
  CONTEXT {
    ENTITIES DeployRequest {
      environment: string
      tests_passed: bool
      approvals: int
      author_approved_own_change: bool
      error_rate_pct: float
      change_freeze: bool
    }
    CONSTANTS {
      min_approvals: int == 2 [SOURCE:ENG-HANDBOOK-7.1]
      max_error_rate_pct: float == 1.5 [SOURCE:SRE-SLO-3]
    }
  }
  TRIGGER {
    deploy()
    rollback()
  }
  GUARANTEE {
    ENSURE deploy() WHEN DeployRequest.tests_passed == true [SOURCE:tests_green]
    ENSURE deploy() WHEN DeployRequest.approvals >= min_approvals [SOURCE:two_approvals]
  }
  PROHIBITION {
    FORBID deploy WHEN DeployRequest.change_freeze == true AND DeployRequest.environment == "production" [SOURCE:change_freeze]
    FORBID deploy WHEN DeployRequest.author_approved_own_change == true [SOURCE:no_self_approval]
    FORBID deploy WHEN DeployRequest.error_rate_pct > max_error_rate_pct [SOURCE:error_budget]
    FORBID rollback WHEN DeployRequest.environment != "production" AND DeployRequest.environment != "staging" [SOURCE:known_environments]
  }
  OBSERVABILITY {
    LOG "deploy decision" ON decision
  }
}

2. Check it

check parses and validates every contract in a file, and prints each one's SHA-256 digest:

example projectreal output
$ ./ulang-lite check policy/deploy.ul
policy/deploy.ul: ok ProductionDeploy (6 clauses; actions deploy, rollback; sha256 686358ddc154b77b5b7b3a24870b8dd6f79b23d376ed039d6824a923284e4059)

A mistake is reported with its position and a stable code. In this copy, error_rate_pct is misspelled:

example projectreal output
$ ./ulang-lite check scratch/typo.ul
scratch/typo.ul:29:24: unsupported[unknown-field]: unknown field DeployRequest.error_rate
[exit status 1]

Every code has its own page, for example unsupported[unknown-field].

3. Decide

Facts are JSON: one object, named after the contract's entity.

facts/freeze.jsonexample project
{
  "DeployRequest": {
    "environment": "production",
    "tests_passed": true,
    "approvals": 2,
    "author_approved_own_change": false,
    "error_rate_pct": 0.4,
    "change_freeze": true
  }
}
example projectreal output
$ ./ulang-lite eval policy/deploy.ul --action deploy --facts facts/freeze.json
{"verdict":"DENY","clause_id":"ProductionDeploy.change_freeze","contract_sha256":"686358ddc154b77b5b7b3a24870b8dd6f79b23d376ed039d6824a923284e4059"}

The change freeze applies only to deploy, so the same facts permit a rollback:

example projectreal output
$ ./ulang-lite eval policy/deploy.ul --action rollback --facts facts/freeze.json
{"verdict":"PERMIT","clause_id":"ProductionDeploy.rollback.permit","contract_sha256":"686358ddc154b77b5b7b3a24870b8dd6f79b23d376ed039d6824a923284e4059"}

Facts are checked against the entity's types before anything is decided:

example projectreal output
$ ./ulang-lite eval policy/deploy.ul --action deploy --facts facts/bad-type.json
facts/bad-type.json: invalid type or domain for DeployRequest.approvals
[exit status 1]

4. Call it from code

src/policy.tsexample project
// Load a µLang v1 contract once, then decide many requests with it.
import { checkFacts, compileSource, contractDigest, formatDiagnostic } from '../vendor/ulang-lite/src/index.ts';

export interface Outcome {
  readonly verdict: 'PERMIT' | 'DENY';
  readonly clause_id: string;
  readonly contract_sha256: string;
}
export interface Rejected { readonly error: string }

export function loadPolicy(source: string, file = 'policy.ul') {
  const result = compileSource(source);
  if (!result.ok) throw new Error(formatDiagnostic(result.diagnostic, file));
  if (result.contracts.length !== 1) throw new Error(`${file}: expected one contract, found ${result.contracts.length}`);
  const valid = result.valid[0]!;
  const contract = result.contracts[0]!;
  const digest = contractDigest(valid.contract);
  return {
    name: contract.name,
    entity: contract.entity,
    actions: contract.actions,
    digest,
    /** Check the facts against the contract's entity, then decide. Bad input is returned, never thrown. */
    decide(action: string, facts: unknown): Outcome | Rejected {
      if (!contract.actions.has(action)) return { error: `${contract.name} does not govern ${action}` };
      const problem = checkFacts(valid, facts);
      if (problem) return { error: problem };
      const values = (facts as Record<string, Record<string, string | number | boolean>>)[contract.entity]!;
      const decision = contract.decide(action, values);
      return { verdict: decision.verdict, clause_id: decision.clause_id, contract_sha256: digest };
    },
  };
}

compileSource runs the whole pipeline once: bounds scan, parse, executable subset, validation and compilation. After that, decide is a few comparisons.

Next: serve it from Node.js or Cloudflare Workers, or follow the tutorial, which builds this contract one rule at a time.