adachsoft/agent-rule-contract
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:
- An implementation of
RuleReaderInterface loads raw rule definitions from your storage (global and project-scoped, both carrying appropriate projectId values).
- It converts them into
Rule objects and returns them wrapped in a RuleCollection.
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
-
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.
-
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.
-
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.
-
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.
-
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
-
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.
-
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.
-
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).
-
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).
-
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.
-
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.
-
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.