Module

rendering.plugins.directives.contracts

Directive contract system for nesting validation.

Provides DirectiveContract to define valid parent-child relationships between directives, catching invalid nesting at parse time rather than producing silent failures or broken HTML.

Architecture:

Contracts are defined as class-level constants on BengalDirective
subclasses. The base class validates contracts automatically during
parsing, emitting warnings for violations.

Related:

  • bengal/rendering/plugins/directives/base.py: BengalDirective validates contracts
  • RFC: plan/active/rfc-directive-system-v2.md (Issue 7: No nested directive validation)

Example:

class StepDirective(BengalDirective):
    CONTRACT = DirectiveContract(requires_parent=("steps",))

# This produces a warning at parse time:
# :::{step}
# Orphaned step - not inside :::{steps}
# :::

Classes

DirectiveContract dataclass
Defines valid nesting relationships for a directive. This is the KEY FEATURE that solves the neste…
3

Defines valid nesting relationships for a directive.

This is the KEY FEATURE that solves the nested directive validation problem. Contracts are checked at parse time to catch invalid nesting early.

Attributes

Name Type Description
requires_parent tuple[str, ...]

This directive MUST be inside one of these parent types. Empty tuple means can appear anywhere (root-level OK).

requires_children tuple[str, ...]

This directive MUST contain at least one of these types. Empty tuple means no required children.

min_children int

Minimum count of required_children types.

max_children int

Maximum children (0 = unlimited). Example - StepDirective (must be inside steps): CONTRACT = DirectiveContract( requires_parent=("steps",), ) Example - StepsDirective (must contain steps): CONTRACT = DirectiveContract( requires_children=("step",), min_children=1, allowed_children=("step",), ) Example - TabSetDirective (tabs with items): CONTRACT = DirectiveContract( requires_children=("tab_item",), min_children=1, )

allowed_children tuple[str, ...]

Only these child types are allowed (whitelist). Empty tuple means any children allowed.

disallowed_children tuple[str, ...]

These child types are NOT allowed (blacklist). Takes precedence over allowed_children.

Methods 2

has_parent_requirement property
True if this directive requires a specific parent.
bool
def has_parent_requirement(self) -> bool

True if this directive requires a specific parent.

Returns

bool

has_child_requirement property
True if this directive requires specific children.
bool
def has_child_requirement(self) -> bool

True if this directive requires specific children.

Returns

bool

Internal Methods 1
__post_init__
Validate contract configuration.
0 None
def __post_init__(self) -> None

Validate contract configuration.

ContractViolation dataclass
Represents a contract violation found during parsing. Collected violations can be: - Logged as war…
1

Represents a contract violation found during parsing.

Collected violations can be:

  • Logged as warnings (default)
  • Raised as errors (strict mode)
  • Reported in health checks

Attributes

Name Type Description
directive str

The directive type that was validated

violation_type str

Type of violation (for structured logging)

message str

Human-readable description

expected list[str] | int | None

What was expected (list, int, or description)

found list[str] | str | int | None

What was actually found

location str | None

Source location (e.g., "content/guide.md:45")

Methods 1

to_log_dict
Convert to structured log format. Note: Uses 'detail' instead of 'message' to …
0 dict[str, Any]
def to_log_dict(self) -> dict[str, Any]

Convert to structured log format.

Note: Uses 'detail' instead of 'message' to avoid conflict with BengalLogger's positional 'message' argument.

Returns

dict[str, Any]

Dict suitable for structured logging kwargs

ContractValidator
Validates directive nesting against contracts. Used by BengalDirective.parse() to check: 1. Parent…
2

Validates directive nesting against contracts.

Used by BengalDirective.parse() to check:

  1. Parent context is valid (if requires_parent specified)
  2. Children meet requirements (if requires_children specified)
  3. Children types are allowed (if allowed_children specified)

Example usage in BengalDirective:

def parse(self, block, m, state):
    # ... parse content ...

    # Validate parent
    if self.CONTRACT:
        parent_type = self._get_parent_type(state)
        violations = ContractValidator.validate_parent(
            self.CONTRACT, self.TOKEN_TYPE, parent_type
        )
        for v in violations:
            self.logger.warning(v.violation_type, **v.to_log_dict())

    # ... parse children ...

    # Validate children
    if self.CONTRACT:
        violations = ContractValidator.validate_children(
            self.CONTRACT, self.TOKEN_TYPE, children
        )
        for v in violations:
            self.logger.warning(v.violation_type, **v.to_log_dict())

Methods 2

validate_parent staticmethod
Validate that the directive is inside a valid parent.
4 list[ContractViolation]
def validate_parent(contract: DirectiveContract, directive_type: str, parent_type: str | None, location: str | None = None) -> list[ContractViolation]

Validate that the directive is inside a valid parent.

Parameters 4
contract DirectiveContract

The directive's contract

directive_type str

The directive being validated (e.g., "step")

parent_type str | None

The parent directive type (None if at root)

location str | None

Source location for error messages

Returns

list[ContractViolation]

List of violations (empty if valid)

validate_children staticmethod
Validate that children meet contract requirements.
4 list[ContractViolation]
def validate_children(contract: DirectiveContract, directive_type: str, children: list[dict[str, Any]], location: str | None = None) -> list[ContractViolation]

Validate that children meet contract requirements.

Parameters 4
contract DirectiveContract

The directive's contract

directive_type str

The directive being validated (e.g., "steps")

children list[dict[str, Any]]

Parsed child tokens

location str | None

Source location for error messages

Returns

list[ContractViolation]

List of violations (empty if valid)