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:
$ ./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:
$ ./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
}
}$ ./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:
$ ./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:
$ ./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.