Authoring Guide

Intent Contracts

Typed, validated mutation contracts with dual targets, composite upserts, and atomic rollback.

Intent Contracts

Intent contracts define typed, validated write operations that modify workspace data while strictly enforcing schema invariants and business rules.

Instead of allowing human customizers or AI agents to make arbitrary, unvalidated edits to raw CSV files or JSON lookup tables, Rescile exposes declarative contracts in contracts/*.toml. Contracts are accessible via:

  1. REST API: POST /api/contracts/:name
  2. GraphQL: Schema-generated mutation resolvers
  3. MCP Tools: invokeContract (discovered via listContracts)

Why Contracts Over Raw Asset Edits?

  1. Intent-Based Boundary: Callers specify what business intent they want to achieve (e.g. ProvisionApplication, AssignFeeGlAccount, AdjustInterestTier), rather than knowing physical file structures or database column mappings.
  2. Strict Input Validation: Fields are verified against regular expressions (pattern), numeric bounds (minimum/maximum), semantic formats (format = "date"), and live graph lookups (enum_lookup).
  3. Dual Mutable Targets: Contracts can mutate either graph entity ground truth (target_asset = "fee_schedules.csv") or reference lookup tables (target_input = "rules.json").
  4. Atomic Rollback: If a contract performs multiple writes across multiple files, all writes are staged in memory. If any write fails, the entire transaction rolls back cleanly without leaving corrupted files.
  5. Deterministic Upsert: Explicit action = "upsert" semantics with composite key matching (key_fields = [...]).

Contract Definition File Structure

Contracts are placed in the contracts/ directory of your data directory or module:

my-project/data/contracts/
├── assign_gl_account.toml
├── update_fee_schedule.toml
└── mutate_determination_rule.toml

Complete Example

name = "UpdateFeeSchedule"
description = "Updates fee schedule parameters or creates a new tier if none exists"

# --- Input Parameter Validation Schema ---
[[input]]
name = "servicing_model"
type = "string"
required = true
# Dynamic validation: input must match an existing code entry in the graph or asset file
enum_lookup = 'code_table_entries[code_table="CODE_SERVICING_MODEL"]:intl_id'

[[input]]
name = "currency"
type = "string"
required = true
pattern = "^[A-Z]{3}$" # Strict 3-letter ISO currency code

[[input]]
name = "rate"
type = "float"
required = true
minimum = 0.0001
maximum = 0.1000

[[input]]
name = "valid_from"
type = "string"
required = true
format = "date" # Validates YYYY-MM-DD date representation

[[input]]
name = "gl_account"
type = "string"
required = false
pattern = "^GL_[0-9]{6}$"

# --- Write Operations ---
[[writes]]
target_asset = "fee_schedules.csv"
action = "upsert"
key_fields = ["servicing_model", "currency", "valid_from"]
record = {
  servicing_model = "{{ input.servicing_model }}",
  currency = "{{ input.currency }}",
  valid_from = "{{ input.valid_from }}",
  rate = "{{ input.rate }}",
  gl_account = "{{ input.gl_account | default(value='GL_DEFAULT_SUSPENSE') }}"
}

Input Validation Directives

Each [[input]] block supports the following constraints:

Field Type Description
name String Parameter name accessed in templates via {{ input.<name> }}.
type String Primitive type: "string", "integer", "float", "boolean", "array", "object".
required Boolean Whether the parameter must be provided.
pattern String Regex pattern used to validate string values (e.g. ^[A-Z]{3}$).
minimum Float / Int Minimum allowable value for numeric inputs (inclusive).
maximum Float / Int Maximum allowable value for numeric inputs (inclusive).
format String Semantic format validation: "date" (YYYY-MM-DD), "timestamp" (RFC3339), "uuid", "email".
allowed_values Array Static enumeration of permitted string or number values.
enum_lookup String Dynamic enumeration DSL query checked against graph nodes or workspace files.

Dynamic enum_lookup

enum_lookup validates that an input value exists dynamically in the active graph or in workspace assets/inputs. It uses a string query expression:

target[filter_property="filter_value"]:property_path
  • target: Vertex label in the in-memory graph (e.g., code_table_entry) or asset filename without extension (e.g., code_table_entries for assets/code_table_entries.csv or input/code_table_entries.json).
  • [filter_property="filter_value"] (optional): Filters records/vertices so only entities where filter_property == filter_value are considered.
  • :property_path (optional, defaults to "name"): Property or column whose values form the allowed enumeration set. Supports dotted path traversal into arrays of objects (e.g. :entries.intl_id).

Examples

1. Filtered CSV Asset / Table Lookup: When a single table or asset contains entries across multiple domains or types (e.g., code table entries), use a filter to restrict allowed values to the relevant group:

[[input]]
name = "servicing_model"
type = "string"
required = true
description = "Internal ID of the servicing model"
enum_lookup = 'code_table_entries[code_table="CODE_SERVICING_MODEL"]:intl_id'

Without the [code_table="..."] filter, code_table_entries:intl_id would collect intl_id from every code table in the file (e.g., client segments, currency codes), allowing unintended values.

2. Direct Graph Entity Lookup: Extracts unique values of code from all vertices of type currency:

[[input]]
name = "currency"
type = "string"
required = true
enum_lookup = "currency:code"

3. Nested Property Array Traversal: If graph vertices or JSON inputs hold nested arrays of objects:

[[input]]
name = "tier_id"
type = "string"
required = true
enum_lookup = "pricing_model[status=active]:tiers.tier_id"

4. Static JSON Lookup Table: Against input/cost_centers.json:

[[input]]
name = "cost_center"
type = "string"
required = true
enum_lookup = "cost_centers:code"

Resolution Order & Best Practices

  1. In-Memory Graph First: Rescile checks loaded graph vertices where vertex.label == target.
  2. Data Directory Fallback: If no vertices match, Rescile looks for assets/<target>.csv or input/<target>.json in the active data directory.
  3. Resource Type vs Asset Name: If an asset is remapped in module.toml via resource_type = "code_table_entry", graph vertices receive the singular label (code_table_entry), while the CSV file on disk remains plural (assets/code_table_entries.csv). Using the asset target name with a filter (e.g., code_table_entries[code_table="..."]:intl_id) ensures correct fallback resolution even when the relation column has been converted into graph edges.

Dual Mutation Targets

Rescile maintains a strict distinction between graph entities and reference lookups:

1. Asset Ground Truth (target_asset)

Modifies CSV files in assets/*.csv. Rows become graph vertices during ingestion.

[[writes]]
target_asset = "applications.csv"
action = "add"
record = { name = "{{ input.name }}", tier = "{{ input.tier }}" }

2. JSON Reference Tables (target_input)

Modifies JSON files in input/*.json. These files serve as reference dictionaries for models via read_input_json or json! without creating graph nodes.

Supports dictionary update (merging keys) and array element updates:

[[writes]]
target_input = "determination_matrix.json"
action = "change"
lookup_by = { rule_code = "{{ input.code }}" }
record = { fallback_rate = "{{ input.new_rate }}" }

Write Actions & Composite Keys

Contracts support four write actions:

  • add: Appends a new record to the target. Fails if a record with the same primary key already exists.
  • change: Modifies an existing record matching lookup_by or key_fields.
  • delete: Removes matching records.
  • upsert: Replaces the matching record if it exists, or appends a new record if it does not.

Composite Keys

For tables where records are identified by multiple fields (e.g., banking determination rules or multi-tenant configurations), pass key_fields:

[[writes]]
target_asset = "fee_determination.csv"
action = "upsert"
key_fields = ["servicing_model", "currency", "valid_from"]
record = {
  servicing_model = "{{ input.servicing_model }}",
  currency = "{{ input.currency }}",
  valid_from = "{{ input.valid_from }}",
  rate = "{{ input.rate }}"
}

Invocation Examples

Via MCP (invokeContract)

{
  "contract_name": "UpdateFeeSchedule",
  "inputs": {
    "servicing_model": "FAMILY_OFFICE",
    "currency": "EUR",
    "rate": 0.015,
    "valid_from": "2026-04-01",
    "gl_account": "GL_400100"
  }
}

Via HTTP REST

curl -X POST http://localhost:8080/api/contracts/UpdateFeeSchedule \
  -H "Content-Type: application/json" \
  -d '{
    "servicing_model": "FAMILY_OFFICE",
    "currency": "EUR",
    "rate": 0.015,
    "valid_from": "2026-04-01"
  }'