Callcium

Policy Validation

Type mismatches, contradictions, redundancies, and vacuities.

The SDK validates policies through semantic analysis. When PolicyBuilder.build() is called, the PolicyValidator inspects every constraint for type errors, logical contradictions, redundancies, and vacuous conditions, then throws a ValidationError carrying every issue if any is found — regardless of severity.

Using validation

PolicyBuilder.build() validates by default and throws a ValidationError — a CallciumError with code VALIDATION_ERROR — carrying every issue found:

import { PolicyBuilder, arg } from "@callcium/sdk";

// Throws ValidationError if issues are found.
const policy = PolicyBuilder
  .create("approve(address,uint256)")
  .add(arg(0).isIn(trustedSpenders))
  .build();

To inspect issues across all severities without throwing, use PolicyBuilder.validate():

const issues = PolicyBuilder
  .create("approve(address,uint256)")
  .add(arg(0).isIn(trustedSpenders))
  .validate();

for (const issue of issues) {
  // Handle each issue: severity, category, code, value1, value2, message.
}

validate() returns an Issue[] covering errors, warnings, and info-level suggestions in a single call. The builder can be inspected multiple times before committing to build().

To encode a policy despite reported issues, use buildUnsafe(). It skips validation entirely — the deliberate escape hatch for a policy whose reported issues you have reviewed and accepted, such as a deliberately redundant test fixture:

const policy = builder.buildUnsafe();

Issue structure

Each issue is an Issue object with the following fields:

FieldTypeDescription
severity"info" | "warning" | "error"Impact level for display and triage. Any issue blocks build().
category"typeMismatch" | "contradiction" | "redundancy" | "vacuity" | "compatibility"Nature of the issue.
codeIssueCodeMachine-readable identifier.
groupIndexnumberIndex of the group holding the rule the issue names.
ruleIndexnumberIndex of that rule within the group: a test, or the loop of a body test.
bodyRuleIndexnumberIndex of the test within its loop's body. Absent for a root rule.
scopeScopeCodeScope of the rule.
pathHexPath of the rule; a body test's path starts at the element.
operatorHexEncoded operator of a test. Absent for a loop.
value1HexFirst value that shows the defect, or zero.
value2HexSecond value that shows the defect, or zero.
messagestringHuman-readable description suitable for logs.

The location fields are absent on an issue about the descriptor. They index the policy's normal form, the one build() encodes: groups and rules sorted, constraints on one array and quantifier joined into one loop, a gte and lte pair on one path joined into one between. They do not follow the order of .add() and .or() calls; match an issue to a constraint by its scope and path.

value1 and value2 carry the values behind the defect, zero where none applies: for example the bound a tighter bound dominates and that tighter bound, or the declared length of a static array beside MAX_QUANTIFIED_ARRAY_LENGTH. Both are Hex strings padded to 32 bytes. Convert with BigInt(value1) for numeric comparisons.

Severity levels

Severity is a presentation and triage signal, not a gating mechanism: build() requires a clean validation result and throws on any issue. buildUnsafe() bypasses validation for all of them.

  • error: the policy is structurally or semantically broken (type mismatch, impossible constraint).
  • warning: the policy is valid but suspicious (e.g. a redundant constraint that is always implied by another).
  • info: an observation that may suggest a clearer formulation.

The strict gate is identical across implementations: the Solidity PolicyBuilder.build() reverts under the same zero-issue rule, and both validators are pinned to each other by the shared conformance vector suite.

Categories

CategoryMeaning
typeMismatchAn operator was applied to an argument type it does not support.
contradictionTwo constraints in the same group cannot both hold (e.g. eq(x) and neq(x)).
redundancyA constraint is implied by another in the same group and adds no additional check.
vacuityA constraint is satisfied by every value (e.g. between(0, 2**256 - 1) on a uint256).
compatibilityA loop crosses a normative limit or holds no test of its element.

Skipping validation

buildUnsafe() is the builder's one path to a blob it has not validated. Tooling that replays opaque blobs or round-trips decoded data can also build a PolicyData object directly and pass it to PolicyCoder.encode, which normalizes the policy without running the validator. This is an advanced path; prefer PolicyBuilder for authored policies.

import { PolicyCoder } from "@callcium/sdk";
import type { PolicyData } from "@callcium/sdk";

const data: PolicyData = {
  /* ... fully populated canonical structure ... */
};
const policy = PolicyCoder.encode(data);

See Policy Inspection for the shape of PolicyData.

On this page