adachsoft / agent-rule-contract
Contract-only PHP library for agent rule definitions (enums, DTOs, collections, exceptions and interfaces) in the AdachSoft AI ecosystem.
Requires
- php: ^8.3
- adachsoft/collection: ^3.0
Requires (Dev)
- adachsoft/changelog-linter: ^0.3.0
- adachsoft/php-code-style: ^0.3.0
- friendsofphp/php-cs-fixer: ^3.89
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.4
- rector/rector: ~2.3.3
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,
- for global rules this MUST be
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 perprojectId), - 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
$projectIdisnull, only global rules MUST be returned, - when
$projectIdis notnull, both global rules and rules for the given project MUST be returned.
- when
getRuleById(RuleId $ruleId): Rule– returns a single rule identified byRuleIdor throwsRuleNotFoundExceptionif 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
RuleIdas the primary identifier of rules in a shared storage containing multiple projects, - honour the
projectIdinvariants enforced byRule(null for global, non-empty string for project).
Methods:
create(Rule $rule): void- MUST throw
RuleAlreadyExistsExceptionwhen a rule with the sameRuleIdalready exists.
- MUST throw
update(Rule $rule): void- MUST NOT change the
RuleIdof an existing rule, - MUST throw
RuleNotFoundExceptionwhen a rule with the givenRuleIddoes not exist.
- MUST NOT change the
delete(RuleId $ruleId): void- MUST throw
RuleNotFoundExceptionwhen the rule does not exist.
- MUST throw
exists(RuleId $ruleId): bool- checks whether a rule with the given
RuleIdexists.
- checks whether a rule with the given
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 (extendsRuntimeException).RuleNotFoundException– thrown when an operation expects a rule with a given identifier to exist, but it cannot be found (typical forgetRuleById(),update(),delete()).RuleAlreadyExistsException– thrown when attempting to create a rule for an identifier that is already present in the storage (typical forcreate()).
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\RuleReaderInterfaceto provide read access to rules, - implement
AdachSoft\AgentRuleContract\Contract\RuleWriterInterfaceif you need to create, update or delete rules in your storage, - build rules as
Ruleinstances and expose them asRuleCollectionfrom your implementations.
A simple example flow:
- An implementation of
RuleReaderInterfaceloads raw rule definitions from your storage (global and project-scoped, both carrying appropriateprojectIdvalues). - It converts them into
Ruleobjects and returns them wrapped in aRuleCollection. RuleWriterInterfaceis used by your application code to create, update or delete rules and to check their existence before operations, usingRuleIdas 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.
- several global rules (
- When calling
getAll() - Then returned
RuleCollectioncontains all of them and enforces uniqueRuleId.
- Given storage contains:
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.
- Given storage contains:
getRuleById returns existing rule
- Given storage contains a rule with
RuleId('rule_1'). - When calling
getRuleById(new RuleId('rule_1')) - Then a
Ruleis returned and its fields (id,projectId,type,target,content, dates) match the stored data.
- Given storage contains a rule with
getRuleById throws RuleNotFoundException for missing rule
- Given storage does not contain a rule with
RuleId('unknown'). - When calling
getRuleById(new RuleId('unknown')) - Then a
RuleNotFoundExceptionis thrown.
- Given storage does not contain a rule with
RuleWriterInterface test cases
create succeeds for new RuleId
- Given storage does not contain a rule with
RuleId('rule_1'). - When calling
create($rule)withid = 'rule_1' - Then the rule is persisted and subsequent
exists(new RuleId('rule_1'))returnstrue.
- Given storage does not contain a rule with
create throws RuleAlreadyExistsException for duplicate RuleId
- Given storage already contains a rule with
RuleId('rule_1'). - When calling
create($rule)again withid = 'rule_1' - Then a
RuleAlreadyExistsExceptionis thrown.
- Given storage already contains a rule with
update succeeds for existing rule
- Given storage contains a rule with
RuleId('rule_1'). - When calling
update($rule)with the sameRuleId('rule_1')but differentcontentormetadata - Then the stored rule is updated (e.g.
getRuleById('rule_1')reflects the new content).
- Given storage contains a rule with
update throws RuleNotFoundException for missing rule
- Given storage does not contain a rule with
RuleId('missing'). - When calling
update($rule)forid = 'missing' - Then a
RuleNotFoundExceptionis thrown and no new rule is implicitly created (no upsert).
- Given storage does not contain a rule with
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')returnsfalse, while subsequentgetRuleById('rule_1')throwsRuleNotFoundException.
- Given storage contains a rule with
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
RuleNotFoundExceptionis thrown.
- Given storage does not contain a rule with
exists reflects actual storage state
- Given storage contains a rule with
RuleId('present')and does not contain a rule withRuleId('absent'). - When calling
exists(new RuleId('present'))andexists(new RuleId('absent')) - Then the first call returns
true, the second returnsfalse.
- Given storage contains a rule with
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.