> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aethis.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Author a rule from legislation

> Agent recipe: create project → upload sources → three-phase generate-and-test → publish.

**You are a coding agent. Given source legislation + test cases, you're producing a deterministic rule ruleset.** This is the authoring workflow.

Three phases — each gated on the previous. Only phase 3 has a TDD loop today; phases 1 and 2 are informal.

Requires an Aethis API key with `projects:write` and `rulesets:write`. Set `AETHIS_API_KEY` in your environment. If you're using MCP, the key goes in the MCP client config (see [MCP install](/mcp-server/overview)).

***

## Phase 1 — Section discovery

Legislation often covers multiple independent criteria. Split each into its own *section* so they can be tested and regenerated separately.

The two interfaces approach this differently. **MCP** has an explicit `aethis_discover_sections` tool the agent calls before authoring. **The CLI** infers section structure at generate time from the source documents plus any guidance you provide — if you want explicit sections, drop one source file per section into `sources/` or add section hints to `guidance/hints.yaml`.

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    aethis init my-rule           # scaffolds aethis.yaml, sources/, tests/, guidance/
    cd my-rule
    # Drop legislation into sources/ — one file per section is the
    # simplest way to make the section split explicit.
    cp ~/Downloads/legislation.pdf sources/
    # Optional: add hints in guidance/hints.yaml to steer the split, e.g.
    #   - "Section A covers age eligibility; Section B covers household means-test."
    ```
  </Tab>

  <Tab title="MCP">
    Ask your coding agent in natural language:

    > *"Use Aethis to split this legislation into independent eligibility sections: \<paste source>."*

    Your agent invokes `aethis_discover_sections`:

    ```
    aethis_discover_sections({
      source_text: "<paste legislation>",
      hint: "Split into independent eligibility gates"
    })
    ```
  </Tab>
</Tabs>

<Note>
  Authoring is multi-step and tightly stateful (project → sources → fields → generate → publish). The CLI and MCP tools orchestrate this for you. The Python SDK does not yet expose authoring endpoints; the raw HTTP endpoints are listed in the [REST API reference](/interfaces/rest-api#author-rules) for completeness but are not the recommended path.
</Note>

***

## Phase 2 — Field vocabulary

Define the input fields each section will consume. Shared vocabulary across sections lets rulebooks compose cleanly.

The two interfaces approach this differently too. **MCP** exposes explicit discovery and pinning tools the agent invokes before generation. **The CLI** infers fields at generate time from your test inputs in `tests/scenarios.yaml` — the field IDs you write into your tests become the field vocabulary. Use prefixed names (`applicant.age`, `household.income`) to keep namespaces tidy across sections.

<Tabs>
  <Tab title="CLI">
    ```yaml theme={null}
    # tests/scenarios.yaml — field IDs you write here become the vocabulary
    tests:
      - name: "eligible — meets age and visa"
        inputs:
          applicant.age: 35
          applicant.visa_status: settled
        expect:
          outcome: eligible
      - name: "not_eligible — student visa"
        inputs:
          applicant.age: 35
          applicant.visa_status: student
        expect:
          outcome: not_eligible
    ```

    Field descriptions and enum constraints come from the source text + any hints in `guidance/hints.yaml` — e.g. *"`applicant.visa_status` is an enum with values student, work, settled."*
  </Tab>

  <Tab title="MCP">
    Ask your agent to discover candidate fields from the section text, then pin them:

    > *"Use Aethis to discover the input fields for project `proj_abc` from this section text, then set `applicant.age` as an int."*

    Your agent invokes:

    ```
    aethis_discover_fields({
      project_id: "proj_abc",
      source_text: "<section text>"
    })

    aethis_set_field_spec({
      project_id: "proj_abc",
      field_id: "applicant.age",
      field_type: "int",
      description: "Applicant age in whole years"
    })
    ```
  </Tab>
</Tabs>

Phase 2 must be settled before generation starts — fields can't be renamed freely once rules reference them.

***

## Phase 3 — Rule generation (TDD loop)

The only phase with an automated test gate. Test-first is non-negotiable: write the tests, THEN generate, THEN iterate until green.

### Step 3.1 — Write tests

Test cases drive everything — the generator iterates until every test passes, the publish endpoint refuses to ship a ruleset with a failing test, and (for the CLI) the field vocabulary is inferred from your test inputs.

The default location after `aethis init` is `tests/scenarios.yaml` for the CLI; for MCP-driven authoring, test cases are supplied inline to `aethis_create_ruleset` or `aethis_generate_and_test`.

```yaml theme={null}
# tests/scenarios.yaml
tests:
  - name: "eligible — meets age and visa"
    inputs:
      applicant.age: 35
      applicant.visa_status: settled
    expect:
      outcome: eligible

  - name: "not_eligible — student visa"
    inputs:
      applicant.age: 35
      applicant.visa_status: student
    expect:
      outcome: not_eligible
```

### Step 3.2 — Generate

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    aethis generate --poll     # creates a draft ruleset, waits for compilation
    ```
  </Tab>

  <Tab title="MCP">
    Ask your agent to run the test-driven generate loop:

    > *"Run aethis\_generate\_and\_test on project `proj_abc`. If any test fails, refine the guidance and retry."*

    Your agent invokes:

    ```
    aethis_generate_and_test({ project_id: "proj_abc" })
    ```

    Each call generates a fresh ruleset and runs all tests against it; the agent loops by calling it again after adjusting source / guidance, not via a `max_iterations` arg on the tool itself.
  </Tab>
</Tabs>

### Step 3.3 — Review + refine

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    aethis test                # runs all test cases against the draft ruleset
    ```

    Refinement in the CLI is iteration on inputs — there is no standalone `refine` command. If a test fails:

    1. Re-read the failing case — was the relevant clause in `sources/` actually clear?
    2. Add a guidance hint in `guidance/hints.yaml`, e.g. *"The age cap is 65, not 60 — see Section 7.2."*
    3. Re-run `aethis generate --poll && aethis test`.

    Rules compile from the source, not from the tests — so you refine the source text or the hints, never the generated rules.
  </Tab>

  <Tab title="MCP">
    Refinement is iteration on inputs from the MCP side too. Ask your agent to add guidance and re-run:

    > *"Add a guidance hint to project `proj_abc`: 'The age cap is 65, not 60 — see Section 7.2.' Then re-run generate-and-test."*

    Your agent invokes:

    ```
    aethis_add_guidance({
      project_id: "proj_abc",
      guidance_text: "The age cap is 65, not 60 — see Section 7.2.",
      process_type: "rule_generation"
    })

    aethis_generate_and_test({ project_id: "proj_abc" })
    ```
  </Tab>
</Tabs>

### Step 3.4 — Publish

Only publish when `aethis test` shows 100% pass. The publish endpoint runs the tests again server-side as a gate.

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    aethis publish --slug my-team/my-rule
    # → ✓ Published ruleset <ruleset_id> — slug: my-team/my-rule
    ```
  </Tab>

  <Tab title="MCP">
    Ask your agent to publish:

    > *"Publish project `proj_abc` under slug `my-team/my-rule` and tell me the resulting `ruleset_id`."*

    Your agent invokes:

    ```
    aethis_publish({
      project_id: "proj_abc",
      slug: "my-team/my-rule"
    })
    ```
  </Tab>
</Tabs>

<Warning>
  The `aethis/*` namespace is reserved for first-party rulesets maintained by Aethis. Use a tenant namespace (e.g. `my-team/*`, `my-company/*`) for your own rulesets. See [Nomenclature](/concepts/nomenclature#the-aethis-namespace).
</Warning>

***

## Composing sections into a rulebook

Once each section has an active ruleset with a stable slug, create the rulebook. See the [worked UK Free School Meals example](/getting-started/examples#uk-free-school-meals) for a full multi-section composition including the `outcome_logic` AST.

```bash theme={null}
curl -X POST https://api.aethis.ai/api/v1/public/rulebooks/ \
  -H "x-api-key: $AETHIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Rulebook",
    "domain": "my_domain",
    "slug": "my-team/my-rulebook",
    "ruleset_refs": [
      { "section_id": "section_a", "pin_mode": "latest_active" },
      { "section_id": "section_b", "pin_mode": "latest_active" }
    ],
    "outcome_logic": {
      "type": "op", "operator": "and",
      "args": [
        { "type": "field_ref", "key": "section_a_group" },
        { "type": "field_ref", "key": "section_b_group" }
      ]
    }
  }'
```

The `outcome_logic` must be a proper Expr AST. The server rejects the text shorthand `{ "expr": "A AND B" }` with HTTP 422 at ingress since aethis-core 0.7.2. To find the group names your sections emit, inspect each ruleset's compiled criteria — or use a known pattern like the one in the FSM example.

***

## Verification checklist

Before declaring a rule complete:

* `aethis test` passes 100% on the ruleset that's about to be published
* `/decide` with the published `ruleset_id` returns the expected decision for each test input
* If the rule is public-facing, `visibility` is `public` (auto-set for `aethis/*` slugs; use `PATCH /rulesets/<id>/visibility` otherwise)
* The ruleset shows up in `GET /api/v1/public/rulesets` (public) or via the authored key (private)

***

## Next steps

* [Debug a failing decide](/recipes/debug-a-decide) — when tests pass but `/decide` surprises you
* [Worked examples](/getting-started/examples) — full projects you can crib from
* [MCP tools reference](/mcp-server/tools) — every authoring tool
