# Advanced Patterns

This page covers patterns that combine multiple Edictum features: nested boolean logic, regex composition, principal claims, template composition, wildcards, dynamic messages, comprehensive contract bundles, per-contract mode overrides, environment-based conditions, and guard merging.

---

## Nested All/Any/Not Logic

Boolean combinators (`all`, `any`, `not`) nest arbitrarily. Use them to build complex access patterns from simple leaves.

**When to use:** Your access contract cannot be expressed as a single condition. You need AND, OR, and NOT logic combined.

=== "YAML"

    ```yaml
    apiVersion: edictum/v1
    kind: ContractBundle

    metadata:
      name: nested-logic

    defaults:
      mode: enforce

    contracts:
      - id: complex-deploy-gate
        type: pre
        tool: deploy_service
        when:
          all:
            - environment: { equals: production }
            - any:
                - principal.role: { not_in: [admin, sre] }
                - not:
                    principal.ticket_ref: { exists: true }
        then:
          effect: deny
          message: "Production deploy denied. Requires (admin or sre role) AND a ticket reference."
          tags: [access-control, production]
    ```

=== "Python"

    ```python
    from edictum import Verdict, precondition

    @precondition("deploy_service")
    def complex_deploy_gate(envelope):
        if envelope.environment != "production":
            return Verdict.pass_()
        # Requires (admin or sre role) AND a ticket reference
        role_ok = envelope.principal and envelope.principal.role in ("admin", "sre")
        has_ticket = envelope.principal and envelope.principal.ticket_ref
        if not role_ok or not has_ticket:
            return Verdict.fail(
                "Production deploy denied. Requires (admin or sre role) "
                "AND a ticket reference."
            )
        return Verdict.pass_()
    ```

**How to read this:** The deploy is denied when the environment is production AND (the role is not admin/sre OR there is no ticket reference). In other words, production deploys require both a privileged role and a ticket.

**Gotchas:**
- Deeply nested trees become hard to read. If your `when` block exceeds three levels of nesting, consider splitting into multiple contracts with simpler conditions.
- `not` takes a single child expression, not an array. `not: [expr1, expr2]` is a validation error.
- Boolean combinators require at least one child in `all` and `any` arrays. An empty array is a validation error.

---

## Regex with matches_any

Combine multiple regex patterns in a single postcondition to detect several categories of sensitive data at once.

**When to use:** You want one contract to catch multiple data patterns (PII, secrets, regulated content) rather than maintaining separate contracts for each.

=== "YAML"

    ```yaml
    apiVersion: edictum/v1
    kind: ContractBundle

    metadata:
      name: regex-composition

    defaults:
      mode: enforce

    contracts:
      - id: comprehensive-data-scan
        type: post
        tool: "*"
        when:
          output.text:
            matches_any:
              - '\\b\\d{3}-\\d{2}-\\d{4}\\b'
              - '\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b'
              - '\\b\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}[\\s-]?\\d{4}\\b'
              - '\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b'
              - 'AKIA[0-9A-Z]{16}'
              - 'eyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+'
        then:
          effect: warn
          message: "Sensitive data pattern detected in output. Redact before using."
          tags: [pii, secrets, compliance]
    ```

=== "Python"

    ```python
    import re
    from edictum import Verdict
    from edictum.contracts import postcondition

    @postcondition("*")
    def comprehensive_data_scan(envelope, tool_response):
        if not isinstance(tool_response, str):
            return Verdict.pass_()
        patterns = [
            r"\b\d{3}-\d{2}-\d{4}\b",                    # SSN
            r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",  # Email
            r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b",  # Credit card
            r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b",             # Phone
            r"AKIA[0-9A-Z]{16}",                           # AWS key
            r"eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+",  # JWT
        ]
        for pat in patterns:
            if re.search(pat, tool_response):
                return Verdict.fail(
                    "Sensitive data pattern detected in output. Redact before using."
                )
        return Verdict.pass_()
    ```

**Gotchas:**
- `matches_any` short-circuits on the first matching pattern. Order patterns from most likely to least likely for performance.
- All patterns are compiled at load time. Invalid regex in any element causes a validation error for the entire bundle.
- Use single-quoted strings in YAML for regex. Double-quoted strings interpret backslash sequences (`\b` becomes backspace, `\d` is literal `d`).

---

## Principal Claims as Dicts

The `principal.claims.<key>` selector accesses custom attributes from the `Principal.claims` dictionary. Claims support any value type: strings, numbers, booleans, and lists.

**When to use:** Your authorization model needs attributes beyond role, user_id, and org_id. Claims let you attach domain-specific metadata like department, clearance level, or capability entitlements.

=== "YAML"

    ```yaml
    apiVersion: edictum/v1
    kind: ContractBundle

    metadata:
      name: claims-patterns

    defaults:
      mode: enforce

    contracts:
      - id: require-clearance
        type: pre
        tool: read_file
        when:
          all:
            - args.path: { contains: "classified" }
            - principal.claims.clearance: { not_in: [secret, top-secret] }
        then:
          effect: deny
          message: "Classified file access requires secret or top-secret clearance."
          tags: [access-control, classified]

      - id: entitlement-gate
        type: pre
        tool: send_email
        when:
          not:
            principal.claims.can_send_email: { equals: true }
        then:
          effect: deny
          message: "Email capability is not enabled for this principal."
          tags: [entitlements]
    ```

    The `entitlement-gate` contract uses principal claims for identity-based gating -- it checks a per-principal boolean attribute, not a feature flag. This is capability enforcement: the principal either has the entitlement or they don't.

=== "Python"

    ```python
    from edictum import Verdict, precondition

    @precondition("read_file")
    def require_clearance(envelope):
        path = envelope.args.get("path", "")
        if "classified" not in path:
            return Verdict.pass_()
        clearance = (
            envelope.principal.claims.get("clearance")
            if envelope.principal else None
        )
        if clearance not in ("secret", "top-secret"):
            return Verdict.fail(
                "Classified file access requires secret or top-secret clearance."
            )
        return Verdict.pass_()

    @precondition("send_email")
    def entitlement_gate(envelope):
        enabled = (
            envelope.principal.claims.get("can_send_email")
            if envelope.principal else False
        )
        if not enabled:
            return Verdict.fail("Email capability is not enabled for this principal.")
        return Verdict.pass_()
    ```

**Setting claims in Python:**

```python
from edictum import Principal

principal = Principal(
    user_id="user-123",
    role="analyst",
    claims={
        "clearance": "secret",
        "department": "engineering",
        "feature_flags_email": True,
    },
)
```

**Gotchas:**
- Claims are set by your application. Edictum does not validate claim values against any external source.
- If a claim key does not exist, the leaf evaluates to `false`. Use `principal.claims.<key>: { exists: false }` to explicitly require a claim.
- Nested claims (e.g., `principal.claims.org.team`) are not supported. Claims are a flat dictionary.

---

## Template Composition

Edictum ships built-in templates that you can load directly. Templates are complete YAML bundles that go through the same validation and hashing path as custom bundles.

**When to use:** You want a ready-made contract bundle for common agent patterns without writing YAML from scratch.

```python
from edictum import Edictum

# Load a built-in template
guard = Edictum.from_template("file-agent")

# Load with overrides
guard = Edictum.from_template(
    "devops-agent",
    environment="staging",
    mode="observe",
)
```

Available templates:

| Template | Description |
|---|---|
| `file-agent` | Blocks sensitive file reads and destructive bash commands |
| `research-agent` | Rate limits, PII detection, and sensitive file protection |
| `devops-agent` | Production gates, ticket requirements, PII detection, session limits |

To customize a template, copy its YAML source from `src/edictum/yaml_engine/templates/` into your project and modify it. Load the customized version with `Edictum.from_yaml()`.

---

## Wildcards

Use `tool: "*"` to target all tools with a single contract. This is useful for cross-cutting concerns that apply regardless of which tool the agent calls.

**When to use:** Security scanning (PII, secrets), session limits, or any contract that should apply to every tool.

=== "YAML"

    ```yaml
    apiVersion: edictum/v1
    kind: ContractBundle

    metadata:
      name: wildcard-patterns

    defaults:
      mode: enforce

    contracts:
      - id: global-pii-scan
        type: post
        tool: "*"
        when:
          output.text:
            matches_any:
              - '\\b\\d{3}-\\d{2}-\\d{4}\\b'
        then:
          effect: warn
          message: "PII detected in {tool.name} output. Redact before using."
          tags: [pii]

      - id: block-all-in-maintenance
        type: pre
        tool: "*"
        when:
          environment: { equals: maintenance }
        then:
          effect: deny
          message: "System is in maintenance mode. All tool calls are denied."
          tags: [maintenance]
    ```

=== "Python"

    ```python
    import re
    from edictum import Verdict, precondition
    from edictum.contracts import postcondition

    @postcondition("*")
    def global_pii_scan(envelope, tool_response):
        if not isinstance(tool_response, str):
            return Verdict.pass_()
        if re.search(r"\b\d{3}-\d{2}-\d{4}\b", tool_response):
            return Verdict.fail(
                f"PII detected in {envelope.tool_name} output. Redact before using."
            )
        return Verdict.pass_()

    @precondition("*")
    def block_all_in_maintenance(envelope):
        if envelope.environment == "maintenance":
            return Verdict.fail("System is in maintenance mode. All tool calls are denied.")
        return Verdict.pass_()
    ```

**Gotchas:**
- Wildcard contracts run on every tool call. In a bundle with many wildcard contracts, each tool call triggers all of them. Keep wildcard contracts lightweight.
- If you need a wildcard contract to exclude specific tools, there is no built-in exclusion syntax. Use a `not` combinator with `tool.name: { in: [...] }` to skip certain tools.

---

## Dynamic Message Interpolation

Messages support `{placeholder}` expansion using the same selector paths as the expression grammar. This makes denial messages specific and actionable.

**When to use:** Always. Generic messages like "Access denied" give the agent no guidance on how to self-correct. Specific messages with interpolated values help the agent understand what went wrong and what to do instead.

=== "YAML"

    ```yaml
    apiVersion: edictum/v1
    kind: ContractBundle

    metadata:
      name: dynamic-messages

    defaults:
      mode: enforce

    contracts:
      - id: detailed-deny-message
        type: pre
        tool: read_file
        when:
          args.path:
            contains_any: [".env", "credentials", ".pem"]
        then:
          effect: deny
          message: "Cannot read '{args.path}' (user: {principal.user_id}, role: {principal.role}). Skip this file."
          tags: [secrets]

      - id: environment-in-message
        type: pre
        tool: deploy_service
        when:
          all:
            - environment: { equals: production }
            - principal.role: { not_in: [admin, sre] }
        then:
          effect: deny
          message: "Deploy to {environment} denied for role '{principal.role}'. Requires admin or sre."
          tags: [access-control]
    ```

=== "Python"

    ```python
    from edictum import Verdict, precondition

    @precondition("read_file")
    def detailed_deny(envelope):
        path = envelope.args.get("path", "")
        for s in (".env", "credentials", ".pem"):
            if s in path:
                user = envelope.principal.user_id if envelope.principal else "unknown"
                role = envelope.principal.role if envelope.principal else "none"
                return Verdict.fail(
                    f"Cannot read '{path}' (user: {user}, role: {role}). Skip this file."
                )
        return Verdict.pass_()

    @precondition("deploy_service")
    def environment_in_message(envelope):
        if envelope.environment != "production":
            return Verdict.pass_()
        if not envelope.principal or envelope.principal.role not in ("admin", "sre"):
            role = envelope.principal.role if envelope.principal else "none"
            return Verdict.fail(
                f"Deploy to {envelope.environment} denied for role '{role}'. "
                "Requires admin or sre."
            )
        return Verdict.pass_()
    ```

**Available placeholders:**
- `{args.<key>}` -- tool argument values
- `{tool.name}` -- the tool being called
- `{environment}` -- the current environment
- `{principal.user_id}`, `{principal.role}`, `{principal.org_id}` -- principal fields
- `{principal.claims.<key>}` -- custom claims
- `{env.<VAR>}` -- environment variable values

**Gotchas:**
- If a placeholder references a missing field, it is kept as-is in the output (e.g., `{principal.user_id}` appears literally if no principal is attached). No error is raised.
- Each placeholder expansion is capped at 200 characters. Values longer than 200 characters are truncated.
- Messages have a maximum length of 500 characters. Keep messages concise.

---

## Combining Pre + Post + Session

A comprehensive contract bundle combines all three contract types: preconditions block before execution, postconditions warn after execution, and session contracts track cumulative behavior.

**When to use:** Production agent deployments where you need defense in depth across all three dimensions.

=== "YAML"

    ```yaml
    apiVersion: edictum/v1
    kind: ContractBundle

    metadata:
      name: comprehensive-governance

    defaults:
      mode: enforce

    contracts:
      # --- Preconditions: block before execution ---
      - id: block-sensitive-reads
        type: pre
        tool: read_file
        when:
          args.path:
            contains_any: [".env", "credentials", ".pem", "id_rsa"]
        then:
          effect: deny
          message: "Sensitive file '{args.path}' denied."
          tags: [secrets, dlp]

      - id: prod-deploy-gate
        type: pre
        tool: deploy_service
        when:
          all:
            - environment: { equals: production }
            - principal.role: { not_in: [admin, sre] }
        then:
          effect: deny
          message: "Production deploys require admin or sre role."
          tags: [access-control, production]

      # --- Postconditions: warn after execution ---
      - id: pii-in-output
        type: post
        tool: "*"
        when:
          output.text:
            matches_any:
              - '\\b\\d{3}-\\d{2}-\\d{4}\\b'
              - '\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b'
        then:
          effect: warn
          message: "PII detected in output. Redact before using."
          tags: [pii, compliance]

      - id: secrets-in-output
        type: post
        tool: "*"
        when:
          output.text:
            matches_any:
              - 'AKIA[0-9A-Z]{16}'
              - 'eyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+'
        then:
          effect: warn
          message: "Credentials detected in output. Do not log or reproduce."
          tags: [secrets, dlp]

      # --- Session: track cumulative behavior ---
      - id: session-limits
        type: session
        limits:
          max_tool_calls: 50
          max_attempts: 120
          max_calls_per_tool:
            deploy_service: 3
            send_email: 10
        then:
          effect: deny
          message: "Session limit reached. Summarize progress and stop."
          tags: [rate-limit]
    ```

=== "Python"

    ```python
    from edictum import Edictum, OperationLimits, Principal, Verdict, precondition
    from edictum.contracts import postcondition
    import re

    @precondition("read_file")
    def block_sensitive_reads(envelope):
        path = envelope.args.get("path", "")
        for s in (".env", "credentials", ".pem", "id_rsa"):
            if s in path:
                return Verdict.fail(f"Sensitive file '{path}' denied.")
        return Verdict.pass_()

    @precondition("deploy_service")
    def prod_deploy_gate(envelope):
        if envelope.environment != "production":
            return Verdict.pass_()
        if not envelope.principal or envelope.principal.role not in ("admin", "sre"):
            return Verdict.fail("Production deploys require admin or sre role.")
        return Verdict.pass_()

    @postcondition("*")
    def pii_in_output(envelope, tool_response):
        if not isinstance(tool_response, str):
            return Verdict.pass_()
        patterns = [r"\b\d{3}-\d{2}-\d{4}\b", r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"]
        for pat in patterns:
            if re.search(pat, tool_response):
                return Verdict.fail("PII detected in output. Redact before using.")
        return Verdict.pass_()

    @postcondition("*")
    def secrets_in_output(envelope, tool_response):
        if not isinstance(tool_response, str):
            return Verdict.pass_()
        patterns = [r"AKIA[0-9A-Z]{16}", r"eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+"]
        for pat in patterns:
            if re.search(pat, tool_response):
                return Verdict.fail("Credentials detected in output. Do not log or reproduce.")
        return Verdict.pass_()

    guard = Edictum(
        contracts=[block_sensitive_reads, prod_deploy_gate, pii_in_output, secrets_in_output],
        limits=OperationLimits(
            max_tool_calls=50,
            max_attempts=120,
            max_calls_per_tool={"deploy_service": 3, "send_email": 10},
        ),
        principal=Principal(role="analyst"),
    )
    ```

**Gotchas:**
- Contract evaluation order within a type follows the array order in the YAML. For preconditions, the first matching deny wins and stops evaluation.
- Postconditions always run, even if the tool was already denied by a precondition. However, if a precondition denies the call, the tool does not execute, so there is no output for the postcondition to inspect.
- Session contracts are checked on every tool call attempt, even before preconditions evaluate.

---

## Per-Contract Mode Override

Individual contracts can override the bundle's default mode. This lets you mix enforced and observed contracts in a single bundle.

**When to use:** You are adding a new contract to an existing production bundle and want to shadow-test it before enforcing.

=== "YAML"

    ```yaml
    apiVersion: edictum/v1
    kind: ContractBundle

    metadata:
      name: mixed-mode-bundle

    defaults:
      mode: enforce

    contracts:
      # Enforced (inherits bundle default)
      - id: block-sensitive-reads
        type: pre
        tool: read_file
        when:
          args.path:
            contains_any: [".env", "credentials"]
        then:
          effect: deny
          message: "Sensitive file denied."
          tags: [secrets]

      # Observe mode: shadow-testing a new contract
      - id: experimental-query-limit
        type: pre
        mode: observe
        tool: query_database
        when:
          args.query: { matches: '\\bSELECT\\s+\\*\\b' }
        then:
          effect: deny
          message: "SELECT * detected (observe mode). Use explicit column lists."
          tags: [experimental, sql-quality]
    ```

=== "Python"

    ```python
    from edictum import Edictum, Verdict, precondition

    @precondition("read_file")
    def block_sensitive_reads(envelope):
        path = envelope.args.get("path", "")
        for s in (".env", "credentials"):
            if s in path:
                return Verdict.fail("Sensitive file denied.")
        return Verdict.pass_()

    # In Python, per-contract mode override is done by running
    # separate Edictum instances: one enforced, one in observe mode.
    enforced_guard = Edictum(contracts=[block_sensitive_reads])

    # Or use a single observe-mode instance to shadow-test:
    import re

    @precondition("query_database")
    def experimental_query_limit(envelope):
        query = envelope.args.get("query", "")
        if re.search(r"\bSELECT\s+\*\b", query):
            return Verdict.fail("SELECT * detected. Use explicit column lists.")
        return Verdict.pass_()

    observe_guard = Edictum(
        mode="observe",
        contracts=[experimental_query_limit],
    )
    ```

**Gotchas:**
- Observe mode emits `CALL_WOULD_DENY` audit events. The tool call proceeds normally. Review these events before switching to enforce.
- The mode override is per-contract. Other contracts in the same bundle continue to use the bundle default.
- Postconditions are always `warn`, so `mode: observe` has no visible effect on postconditions. Observe mode is meaningful only for preconditions and session contracts.

---

## Environment-Based Conditions

Use `env.*` selectors to conditionally activate contracts based on environment variables. The evaluator reads `os.environ` at evaluation time -- no adapter changes, no envelope modifications, no code changes.

**When to use:** You want a single YAML file with contracts that activate based on runtime flags like `DRY_RUN`, `ENVIRONMENT`, or `FEATURE_X_ENABLED`. Set the env var, and the contract activates.

=== "YAML"

    ```yaml
    apiVersion: edictum/v1
    kind: ContractBundle

    metadata:
      name: env-conditions

    defaults:
      mode: enforce

    contracts:
      # Block modifications when DRY_RUN is set
      - id: dry-run-block
        type: pre
        tool: "*"
        when:
          all:
            - env.DRY_RUN: { equals: true }
            - tool.name: { in: [Bash, Write, Edit] }
        then:
          effect: deny
          message: "Dry run mode — modifications denied."
          tags: [dry-run]

      # Block destructive commands in production
      - id: prod-destructive-block
        type: pre
        tool: Bash
        when:
          all:
            - env.ENVIRONMENT: { equals: "production" }
            - args.command: { matches: '\brm\s+(-rf?|--recursive)\b' }
        then:
          effect: deny
          message: "Destructive commands denied in {env.ENVIRONMENT}."
          tags: [destructive, production]
    ```

=== "Usage"

    ```bash
    # Activate dry-run mode
    DRY_RUN=true python agent.py

    # Or set environment
    ENVIRONMENT=production python agent.py
    ```

**Type coercion:** Env vars are strings, but the evaluator coerces them automatically:

- `"true"` / `"false"` (case-insensitive) become `True` / `False`
- Numeric strings like `"42"` or `"3.14"` become `int` or `float`
- Everything else stays a string

This means `env.DRY_RUN: { equals: true }` works when `DRY_RUN=true` is set -- you compare against the boolean `true`, not the string `"true"`.

**Gotchas:**
- Unset env vars evaluate to `false` (the contract does not fire). This is consistent with how missing fields behave everywhere in Edictum.
- Env vars are read at evaluation time, not load time. If an env var changes mid-process, the next tool call sees the new value.
- All 15 operators work with `env.*` selectors. Use numeric operators (`gt`, `lt`, etc.) with coerced numeric env vars.
- `{env.VAR_NAME}` works in message templates for dynamic denial messages.

---

## Bundle Composition (Multi-File)

`Edictum.from_yaml()` accepts multiple paths, composing bundles left-to-right with deterministic merge semantics. This is the preferred way to combine contracts from multiple YAML files.

**When to use:** You have separate YAML files for different concerns (base safety, team overrides, environment-specific contracts) and want to compose them into a single guard.

```python
from edictum import Edictum

guard = Edictum.from_yaml(
    "contracts/base.yaml",
    "contracts/team-overrides.yaml",
    "contracts/prod-overrides.yaml",
)
```

**Merge semantics:**

- Contracts with the same `id`: later layer replaces the entire contract
- Contracts with unique IDs: concatenated into the final list
- `defaults.mode`, `limits`, `observability`: later layer wins
- `tools`, `metadata`: deep merge (later keys override)

Use `return_report=True` to see what was overridden:

```python
guard, report = Edictum.from_yaml(
    "contracts/base.yaml",
    "contracts/overrides.yaml",
    return_report=True,
)

for o in report.overridden_contracts:
    print(f"{o.contract_id}: overridden by {o.overridden_by}")
```

**Shadow-testing with `observe_alongside`:**

A second bundle with `observe_alongside: true` evaluates as shadow contracts -- audit events are emitted but tool calls are never denied:

```python
guard = Edictum.from_yaml(
    "contracts/current.yaml",      # enforced
    "contracts/candidate.yaml",    # observe_alongside: true → shadow
)
```

See [Bundle Composition](../yaml-reference.md#bundle-composition) for full reference.

**Gotchas:**
- Contract replacement is by `id`, not position. No partial merging of conditions within a contract.
- Single-path `from_yaml("file.yaml")` is unchanged and backward compatible.
- For composing Python-defined guards at runtime, use `Edictum.from_multiple()`.

---

## Guard Merging (Python)

Use `Edictum.from_multiple()` to combine contracts from multiple instantiated guards into a single guard. This is for runtime merging of Python-defined contracts or conditionally loaded YAML guards.

**When to use:** You need to combine guards at the Python level -- for example, conditionally adding guards based on runtime state, or mixing YAML-loaded and Python-defined contracts.

```python
import os
from edictum import Edictum

guards = [Edictum.from_yaml("contracts/base.yaml")]

if os.environ.get("DRY_RUN"):
    guards.append(Edictum.from_yaml("contracts/dry-run.yaml"))

guard = Edictum.from_multiple(guards)
```

!!! note "Prefer `from_yaml(*paths)` for YAML composition"
    If you are combining multiple YAML files, use `from_yaml("base.yaml", "overrides.yaml")` instead of `from_multiple()`. Multi-path `from_yaml()` provides deterministic merge semantics, composition reports, and `observe_alongside` support. `from_multiple()` is for cases where you need runtime conditional loading or mixing Python-defined contracts.

**Semantics:**

- Contracts are concatenated in order. The first guard's contracts evaluate first.
- The first guard's audit config, mode, environment, and limits are used as the base.
- Duplicate contract IDs: first occurrence wins, duplicates skipped with a warning.
- The returned guard is a new instance. Input guards are not mutated.

**Gotchas:**
- `from_multiple([])` raises `EdictumConfigError`. At least one guard is required.
- Hooks (before/after) are not merged -- only contracts (preconditions, postconditions, session contracts).
- Duplicate IDs are checked across all contract types. A precondition ID in guard A blocks a postcondition with the same ID in guard B.
