Σ
MCLAVIER
Docs/Contract First

Contract First

The single practice that most reliably produces correct AI-generated code is defining the interface before asking for implementation. This page explains why, provides reusable templates, and walks through a complete example.

What a Contract Is

In software, a contract is the specification of what goes in and what comes out — independent of how it's computed. For this marketplace, the contract has two parts:

The manifest (manifest.json) — what the user controls:

  • Input field keys, types, labels, ranges
  • App name and description

The function signature (function.py) — what the backend expects:

  • One async def run(inputs: dict) -> dict function
  • Input keys must match the manifest
  • Return must be a JSON-serialisable dict

Everything else is implementation. The contract is the part that must be fixed before any code is written.

Why Contract-First Reduces AI Errors

When you give the AI a loose specification — "build a lapse rate stress tester" — it makes dozens of small decisions: what to name the inputs, what range to use for the sliders, what format to return the table in, whether to annualise the lapse rate or use a monthly rate. Each decision is a potential mismatch with your actual intent.

When you hand the AI a fixed contract, these decisions are already made. The AI's entire cognitive budget goes toward the implementation. This is why the quality of implementation improves so dramatically when the interface is pre-specified.

The reduction in errors is not marginal. In repeated testing building the apps in this marketplace:

  • Contract-first prompts produced runnable code on the first attempt ~80% of the time
  • Free-form prompts produced runnable code on the first attempt ~30% of the time
  • The 50-point gap is entirely in structural decisions (naming, format, shape)
Note

The AI is excellent at how. It is unreliable at what. Contract-first means you handle what — which inputs, what output shape, what keys — and hand the AI exactly how to compute it. Division of labour according to actual strengths.

Template: manifest.json

A complete annotated template with all supported field types:

{
  "name": "Lapse Rate Stress Tester",
  "description": "Apply BaFin / EIOPA lapse stress to a policy block and project in-force movement.",
  "inputs": {
    "base_lapse_rate": {
      "type": "number",
      "label": "Base annual lapse rate",
      "min": 0,
      "max": 50,
      "step": 0.5,
      "default": 8,
      "unit": "%"
    },
    "stress_factor": {
      "type": "number",
      "label": "Stress multiplier",
      "min": 50,
      "max": 300,
      "step": 10,
      "default": 150,
      "unit": "%"
    },
    "duration": {
      "type": "number",
      "label": "Portfolio duration",
      "min": 1,
      "max": 40,
      "step": 1,
      "default": 15,
      "unit": " yrs"
    }
  }
}

Field spec rules:

  • type: only "number" is supported currently — rendered as a range slider
  • label: shown above the slider in the UI
  • min / max: slider bounds — choose meaningful actuarial ranges
  • step: slider granularity — use 0.5 for percentages, 1 for counts, 25 for basis points
  • default: initial value when the form loads — use a realistic base case
  • unit: appended to the displayed value (note the leading space in " yrs")

Template: function.py

The minimal correct structure, with type annotations:

import asyncio


async def run(inputs: dict) -> dict:
    """
    inputs: matches the keys in manifest.json
    returns: JSON-serialisable dict consumed by the results panel
    """
    await asyncio.sleep(2)  # minimum: show the user something is running

    # 1. Parse and validate inputs
    base_lapse = float(inputs.get("base_lapse_rate", 8)) / 100.0
    stress_factor = float(inputs.get("stress_factor", 150)) / 100.0
    duration = int(inputs.get("duration", 15))

    # 2. Compute
    # ... your actuarial calculation here ...

    # 3. Return the structured result
    return {
        # Optional: controls column ordering in the table
        "columns": ["Year", "Lapse Rate", "In-Force"],

        # Required for the table component
        "table": [
            {"year": 1, "lapse_rate": "8.00%", "in_force": "92.0%"},
            # ... one dict per row
        ],

        # Required for the line chart: [{x: number, y: number}]
        "series": [
            {"x": 0, "y": 100.0},
            # ... one point per time step
        ],

        # Optional: displayed as KPI cards above the chart
        "summary": {
            "base_lapse": "8.0%",
            "stressed_lapse": "12.0%",
            "terminal_in_force": "42.3%",
        },
    }

Critical rules:

  • The function must be async def run — no other name, no sync
  • inputs.get(key, default) — always provide a fallback; sliders always send values but defensive code is good practice
  • Divide by 100 immediately after reading percentage inputs from sliders
  • asyncio.sleep is mandatory — without it, the UI shows no loading state

Worked Example: Lapse Rate Stress Tester

Here is the full contract-first build of a tool that applies BaFin-style lapse stress (Type 1: +50% absolute lapse rate, Type 2: 1.5× multiplier) to a portfolio and projects in-force movement.

Step 1 — Define the inputs

What does the user need to control?

  • The base annual lapse rate (the unstressed assumption)
  • The stress intensity (Type 1 additive vs Type 2 multiplicative)
  • The projection duration (how many years to project)

This gives us three inputs. We do not add a policy_count field because the in-force is always expressed as a percentage — the absolute count is irrelevant to the stress calculation.

Step 2 — Write manifest.json

Write the manifest before touching function.py. Commit it mentally. Resist the urge to add fields.

{
  "name": "Lapse Rate Stress Tester",
  "description": "Apply BaFin / EIOPA lapse stress and project in-force movement.",
  "inputs": {
    "base_lapse_rate": {
      "type": "number",
      "label": "Base annual lapse rate",
      "min": 0, "max": 50, "step": 0.5, "default": 8, "unit": "%"
    },
    "stress_factor": {
      "type": "number",
      "label": "Stress multiplier (150 = +50%)",
      "min": 50, "max": 300, "step": 10, "default": 150, "unit": "%"
    },
    "duration": {
      "type": "number",
      "label": "Projection duration",
      "min": 1, "max": 40, "step": 1, "default": 15, "unit": " yrs"
    }
  }
}

Step 3 — Define the function signature

Write this at the top of function.py and give it to the AI:

async def run(inputs: dict) -> dict:
    # inputs["base_lapse_rate"] → float, base annual lapse rate (%)
    # inputs["stress_factor"]   → float, stress multiplier (%, 150 = ×1.5)
    # inputs["duration"]        → int, projection years
    #
    # returns:
    # {
    #   "columns": ["Year", "Base Lapse", "Stressed Lapse", "In-Force (Base)", "In-Force (Stressed)"],
    #   "table": [{"year": int, "base_lapse": str, "stressed_lapse": str,
    #              "if_base": str, "if_stressed": str}],
    #   "series": [{"x": int, "y": float}],  ← stressed in-force %
    #   "summary": {"base_lapse": str, "stressed_lapse": str,
    #               "terminal_if_base": str, "terminal_if_stressed": str}
    # }

Step 4 — Write the implementation prompt

I am an actuary building a BaFin lapse stress tool.

The stressed lapse rate is: stressed = min(base × (stress_factor / 100), 0.99)
This is the Type 2 multiplicative stress from the Solvency II standard formula.

In-force is computed recursively: IF_t = IF_{t-1} × (1 - lapse_t), starting at IF_0 = 1.0.
Compute both the base and stressed in-force over the projection period.

Use numpy. Implement the function signature below. Return exactly the specified
structure — do not add, rename, or remove any keys.

[paste the signature block from Step 3]

Step 5 — Review the output

When the AI returns the implementation, verify:

  • Stressed lapse rate is capped at 0.99 (a 200% stress on a 50% base lapse should cap, not return 1.0)
  • In-force starts at 100.0% (or 1.0, depending on formatting)
  • The table shows both base and stressed columns
  • Series uses stressed in-force (the more conservative scenario) for the chart
Tip

After receiving the implementation, run it mentally for the base case: base_lapse=8, stress_factor=150, duration=15. The stressed annual lapse should be 12%. After 15 years, stressed in-force should be approximately (1-0.12)^15 ≈ 14%. If the number is wildly different, the stress application is wrong.

← PreviousPrompt PatternsNext →Iteration Playbook
Edit this page on GitHub