adachsoft/agent-rule-contract

Contract-only PHP library for agent rule definitions (enums, DTOs, collections, exceptions and interfaces) in the AdachSoft AI ecosystem.

Maintainers

Package info

gitlab.com/a.adach/agent-rule-contract

Issues

pkg:composer/adachsoft/agent-rule-contract

Statistics

Installs: 13

Dependents: 2

Suggesters: 0

Stars: 0

v0.6.0 2026-02-16 11:36 UTC

This package is not auto-updated.

Last update: 2026-03-02 10:50:44 UTC


README

Contract-only PHP library defining enums, value objects, collections, exceptions and interfaces for agent rule management in the AdachSoft AI ecosystem.

Installation

composer require adachsoft/agent-rule-contract

Overview

This package provides stable contracts for building rule-aware agents:

  • enums describing rule type and target,
  • immutable rule identifier value object (RuleId),
  • immutable rule value object (Rule),
  • immutable rule collection (RuleCollection),
  • domain exceptions for the rule subsystem,
  • interfaces for reading and writing rules.

The library does not ship any runtime implementation of storage or rule evaluation. It is intended to be consumed by higher-level components that implement these interfaces.

Enums

  • RuleTypeEnum – describes the normative nature of a rule:
    • REQUIREMENT – something the agent must do,
    • PROHIBITION – something the agent must not do,
    • GUIDELINE – soft recommendation or best practice,
    • CONSTRAINT – hard technical or business constraint.
  • RuleTargetEnum – describes what the rule targets:
    • AGENT – agent behaviour in general,
    • TOOL – usage of external tools,
    • CODE – source code generation or modification,
    • OUTPUT – final responses / visible output.

These enums are part of the public contract and should be used consistently across all rule-related components.

RuleId value object

AdachSoft\AgentRuleContract\ValueObject\RuleId is a small immutable value object representing a validated rule identifier.

A valid RuleId:

  • consists only of lowercase letters (a-z), digits (0-9) and underscores (_),
  • has a minimum length of 3 characters.

An attempt to construct RuleId with an invalid value will result in RuleException.

Rule value object

AdachSoft\AgentRuleContract\ValueObject\Rule is a readonly immutable value object representing a single rule:

  • id (RuleId) – stable identifier of the rule (unique within your system),
  • type (RuleTypeEnum) – semantic type of the rule,
  • projectId (?string) – project identifier for project-scoped rules:
    • for global rules this MUST be null,
    • for project rules this MUST be a non-empty string uniquely identifying the owning project,
  • target (RuleTargetEnum) – primary target of the rule,
  • content (string) – human- or machine-readable rule content (MUST be at least 10 characters long),
  • priority (int) – optional priority used by consumers to order or select rules (default is implementation-defined),
  • createdAt (\DateTimeImmutable) – creation datetime,
  • updatedAt (\DateTimeImmutable) – last update datetime,
  • metadata (KeyedCollectionInterface<string, mixed>|null) – optional key-value collection with additional data (e.g. tags, origin, audit information).

The constructor of Rule enforces consistency of projectId and minimal content length and will throw RuleException if these invariants are violated.

Consumers should treat Rule as an immutable snapshot and avoid mutating any referenced metadata collections in-place.

Rule collection

AdachSoft\AgentRuleContract\Collection\RuleCollection is an immutable collection of Rule instances. It:

  • guarantees that all items are instances of Rule,
  • enforces that there are no duplicate rules with the same RuleId (IDs are global, not per projectId),
  • provides value-object style behaviour (no in-place modifications),
  • can be safely passed between layers and components.

Interfaces

The following interfaces define the integration points for your application:

RuleReaderInterface

High-level read-side contract for obtaining rules:

  • getAll(): RuleCollection – returns all rules (global and project-specific) across all projects.
  • getAllByProject(?string $projectId): RuleCollection – returns all rules for the given project:
    • when $projectId is null, only global rules MUST be returned,
    • when $projectId is not null, both global rules and rules for the given project MUST be returned.
  • getRuleById(RuleId $ruleId): Rule – returns a single rule identified by RuleId or throws RuleNotFoundException if it cannot be found.

RuleWriterInterface

Write-side contract responsible for creating, updating and deleting rules, as well as checking their existence in the underlying storage.

Implementations MUST:

  • use RuleId as the primary identifier of rules in a shared storage containing multiple projects,
  • honour the projectId invariants enforced by Rule (null for global, non-empty string for project).

Methods:

  • create(Rule $rule): void
    • MUST throw RuleAlreadyExistsException when a rule with the same RuleId already exists.
  • update(Rule $rule): void
    • MUST NOT change the RuleId of an existing rule,
    • MUST throw RuleNotFoundException when a rule with the given RuleId does not exist.
  • delete(RuleId $ruleId): void
    • MUST throw RuleNotFoundException when the rule does not exist.
  • exists(RuleId $ruleId): bool
    • checks whether a rule with the given RuleId exists.

Implementations of these interfaces live in your application or infrastructure code and are outside the scope of this package.

Exceptions

The library exposes a small hierarchy of domain exceptions:

  • RuleException – base exception for all rule-related errors (extends RuntimeException).
  • RuleNotFoundException – thrown when an operation expects a rule with a given identifier to exist, but it cannot be found (typical for getRuleById(), update(), delete()).
  • RuleAlreadyExistsException – thrown when attempting to create a rule for an identifier that is already present in the storage (typical for create()).

These exceptions can be used by your implementations of the interfaces to signal domain-specific error conditions in a consistent way.

Usage

In your project:

  • implement AdachSoft\AgentRuleContract\Contract\RuleReaderInterface to provide read access to rules,
  • implement AdachSoft\AgentRuleContract\Contract\RuleWriterInterface if you need to create, update or delete rules in your storage,
  • build rules as Rule instances and expose them as RuleCollection from your implementations.

A simple example flow:

  1. An implementation of RuleReaderInterface loads raw rule definitions from your storage (global and project-scoped, both carrying appropriate projectId values).
  2. It converts them into Rule objects and returns them wrapped in a RuleCollection.
  3. RuleWriterInterface is used by your application code to create, update or delete rules and to check their existence before operations, using RuleId as the primary identifier in a multi-project storage.

Testing your implementations

When implementing the contracts, you SHOULD cover at least the following scenarios with tests.

RuleReaderInterface test cases

  1. getAll() returns all rules

    • Given storage contains:
      • several global rules (projectId = null),
      • several project rules for multiple projects.
    • When calling getAll()
    • Then returned RuleCollection contains all of them and enforces unique RuleId.
  2. getAllByProject(null) returns only global rules

    • Given storage contains both global and project-scoped rules.
    • When calling getAllByProject(null)
    • Then returned collection contains only rules with projectId = null.
  3. getAllByProject(projectId) returns global + project rules

    • Given storage contains:
      • global rules,
      • rules for projectId = 'project_1',
      • rules for projectId = 'project_2'.
    • When calling getAllByProject('project_1')
    • Then returned collection contains all global rules plus all rules with projectId = 'project_1', and no rules for other projects.
  4. getRuleById returns existing rule

    • Given storage contains a rule with RuleId('rule_1').
    • When calling getRuleById(new RuleId('rule_1'))
    • Then a Rule is returned and its fields (id, projectId, type, target, content, dates) match the stored data.
  5. getRuleById throws RuleNotFoundException for missing rule

    • Given storage does not contain a rule with RuleId('unknown').
    • When calling getRuleById(new RuleId('unknown'))
    • Then a RuleNotFoundException is thrown.

RuleWriterInterface test cases

  1. create succeeds for new RuleId

    • Given storage does not contain a rule with RuleId('rule_1').
    • When calling create($rule) with id = 'rule_1'
    • Then the rule is persisted and subsequent exists(new RuleId('rule_1')) returns true.
  2. create throws RuleAlreadyExistsException for duplicate RuleId

    • Given storage already contains a rule with RuleId('rule_1').
    • When calling create($rule) again with id = 'rule_1'
    • Then a RuleAlreadyExistsException is thrown.
  3. update succeeds for existing rule

    • Given storage contains a rule with RuleId('rule_1').
    • When calling update($rule) with the same RuleId('rule_1') but different content or metadata
    • Then the stored rule is updated (e.g. getRuleById('rule_1') reflects the new content).
  4. update throws RuleNotFoundException for missing rule

    • Given storage does not contain a rule with RuleId('missing').
    • When calling update($rule) for id = 'missing'
    • Then a RuleNotFoundException is thrown and no new rule is implicitly created (no upsert).
  5. delete removes existing rule

    • Given storage contains a rule with RuleId('rule_1').
    • When calling delete(new RuleId('rule_1'))
    • Then the rule is removed and exists('rule_1') returns false, while subsequent getRuleById('rule_1') throws RuleNotFoundException.
  6. delete throws RuleNotFoundException when rule does not exist

    • Given storage does not contain a rule with RuleId('missing').
    • When calling delete(new RuleId('missing'))
    • Then a RuleNotFoundException is thrown.
  7. exists reflects actual storage state

    • Given storage contains a rule with RuleId('present') and does not contain a rule with RuleId('absent').
    • When calling exists(new RuleId('present')) and exists(new RuleId('absent'))
    • Then the first call returns true, the second returns false.

Implementing and testing these scenarios will ensure that your storage layer honours the contracts defined by this package and behaves predictably for both happy-path and error conditions.