Status: Owner Approved / Runtime Implemented
This document defines the complete approved architecture for replacing the superseded post-v1 SecuritySignals pagination wrapper with a package-owned Admin Query API. The Runtime implementation is now present, covered, and verified.
It records the Owner decisions made on 2026-07-14, the post-v1 retirement rule recorded by ADMIN_QUERY_SECURITY_SIGNALS_POST_V1_RETIREMENT_DECISION.md, and final approval of the complete coherent blueprint on 2026-07-15. It authorizes a separate Runtime implementation task/PR, but it does not itself implement Runtime, tagging, or release work.
- Audit date:
2026-07-14(UTC) - Exact audited
mainSHA:3169947e107df66f61884abb5c95f1dfa621a69b - Original PR #102 HEAD:
d5121cee3a3069aaaaea5dded2521ae1316f5fdb - Pre-Owner-decision corrected HEAD:
c4f3b234e1d82b0b08b8207c76b64f46474ec058 - Regressed Owner-decision commit inspected:
0da1f2f28c5146c84a6cada1c0534eabb401c84a - Historical post-v1 pagination origin: PR #74,
Add SecuritySignals paginated query support
src/SecuritySignals/Contract/SecuritySignalsQueryInterface.phpsrc/SecuritySignals/Contract/SecuritySignalsPolicyInterface.phpsrc/SecuritySignals/DTO/SecuritySignalsQueryDTO.phpsrc/SecuritySignals/DTO/SecuritySignalsViewDTO.phpsrc/SecuritySignals/DTO/SecuritySignalRecordDTO.phpsrc/SecuritySignals/Infrastructure/Mysql/SecuritySignalsQueryMysqlRepository.phpsrc/SecuritySignals/Infrastructure/Mysql/SecuritySignalsLoggerMysqlRepository.phpsrc/SecuritySignals/Recorder/SecuritySignalsRecorder.phpsrc/SecuritySignals/Recorder/SecuritySignalsDefaultPolicy.phpsrc/SecuritySignals/Exception/SecuritySignalsStorageException.phpsrc/SecuritySignals/Database/schema.maa_event_logging_security_signals.sqlsrc/Bootstrap/EventLoggingBindings.phpsrc/Factory/SecuritySignalsFactory.php
tests/Unit/SecuritySignals/Repository/SecuritySignalsQueryMysqlRepositoryTest.phptests/Integration/SecuritySignals/SecuritySignalsRepositoryTest.phptests/Unit/SecuritySignals/DTO/SecuritySignalsQueryCursorDTOTest.phptests/Unit/SecuritySignals/DTO/SecuritySignalsQueryPageDTOTest.phptests/Unit/SecuritySignals/Service/SecuritySignalsPaginatedQueryServiceTest.php
AGENTS.mdEVENT_LOGGING_PACKAGE_REFERENCE.mddocs/standards/PACKAGE_BUILDING_STANDARD.mddocs/standards/COMPOSER_PACKAGE_STANDARD.mddocs/standards/CI_WORKFLOW_STANDARD.mddocs/standards/LIBRARY_PRESENTATION_STANDARD.mddocs/architecture/ADMIN_QUERY_API_ARCHITECTURE.mddocs/architecture/ADMIN_QUERY_AUDIT_TRAIL_POC_BLUEPRINT.mddocs/architecture/ADMIN_QUERY_BEHAVIOR_TRACE_REBUILD_BLUEPRINT.mddocs/architecture/ADMIN_QUERY_SECURITY_SIGNALS_POST_V1_RETIREMENT_DECISION.mddocs/architecture/PRIMITIVE_READ_QUERY_SUPPORT_DESIGN.mddocs/roadmap/ADMIN_QUERY_API_ROADMAP.mddocs/audits/ADMIN_QUERY_PHASE_1_RUNTIME_COMPATIBILITY_INVENTORY.mddocs/audits/DOCUMENTATION_INVENTORY.mddocs/integration/ADMIN_READ_USAGE.md
The audit distinguishes three different contracts:
- Protected
v1.0.0Runtime- primitive public interface;
- primitive query and view DTOs;
- primitive repository constructor and observable behavior;
- schema;
- row hydration;
- storage exception boundary;
- write-side policy behavior;
- recorder fail-open reliability boundary.
- Superseded post-v1 pagination experiment
- four Runtime wrapper artifacts;
- three directly associated unit tests;
- introduced by PR #74;
- not part of the protected
v1.0.0surface; - replaced and deleted atomically inside the approved Runtime rebuild.
- Approved Admin Query path
- SecuritySignals-specific package API;
- offset/page pagination through
maatify/persistence; - no change to the primitive public contract.
Current code, current schema, active canonical documents, the post-v1 retirement decision, and already implemented package-owned Admin Query patterns take precedence over historical documents.
The protected method is exactly:
public function find(SecuritySignalsQueryDTO $query): array;The return contract is:
/** @return array<SecuritySignalsViewDTO> */SecuritySignals has no primitive read() method.
The method may throw SecuritySignalsStorageException.
The protected constructor order and defaults are exactly:
public function __construct(
public ?\DateTimeImmutable $after = null,
public ?\DateTimeImmutable $before = null,
public ?string $actorType = null,
public ?int $actorId = null,
public ?string $signalType = null,
public ?string $severity = null,
public ?string $requestId = null,
public ?string $correlationId = null,
public ?\DateTimeImmutable $cursorOccurredAt = null,
public ?int $cursorId = null,
public int $limit = 50,
)The protected serialized keys and order are:
after
before
actorType
actorId
signalType
severity
requestId
correlationId
cursorOccurredAt
cursorId
limit
Dates serialize using DATE_ATOM.
The protected constructor and serialized field order is exactly:
id
eventId
actorType
actorId
signalType
severity
correlationId
requestId
routeName
ipAddress
userAgent
metadata
occurredAt
occurredAt serializes using DATE_ATOM.
The protected constructor is exactly:
public function __construct(private readonly PDO $pdo)The primitive constructor accepts only PDO.
It must not gain a policy, mapper, paginator, descriptor builder, logger, or testing seam as a public constructor dependency.
The primitive repository supports exactly:
| Query field | SQL condition |
|---|---|
actorType |
actor_type = :actor_type |
actorId |
actor_id = :actor_id |
signalType |
signal_type = :signal_type |
severity |
severity = :severity |
requestId |
request_id = :request_id |
correlationId |
correlation_id = :correlation_id |
after |
occurred_at >= :after |
before |
occurred_at <= :before |
Date parameters use:
Y-m-d H:i:s.u
The cursor condition is activated only when both cursorOccurredAt and cursorId are non-null.
The protected ordering is:
ORDER BY occurred_at DESC, id DESCThe protected limit behavior is:
$limit = max(1, $query->limit);The primitive path currently uses SELECT *. That existing observable behavior is preserved. The new Admin Query path must use an explicit selected-column list.
The primitive repository does not begin, commit, or roll back transactions.
The protected behavior is:
- rows returned by
fetchAll(PDO::FETCH_ASSOC)that are not arrays are skipped; - numeric
idmaps toint; otherwise0; - string
event_idmaps toeventId; otherwise''; - string
actor_typemaps toactorType; otherwisenull; - numeric
actor_idmaps toint; otherwisenull; - string
signal_typemaps tosignalType; otherwise''; - string
severitymaps toseverity; otherwise''; - string
correlation_idmaps tocorrelationId; otherwisenull; - string
request_idmaps torequestId; otherwisenull; - string
route_namemaps torouteName; otherwisenull; - string
ip_addressmaps toipAddress; otherwisenull; - string
user_agentmaps touserAgent; otherwisenull; - missing or non-string
occurred_atis parsed from1970-01-01 00:00:00using UTC; - invalid persisted date text throws during row mapping;
- missing, non-string, or empty
metadatamaps tonull; - malformed JSON maps to
null; - scalar JSON maps to
null; - a JSON array containing any numeric key maps to
null; - an associative JSON object maps to
array<string, mixed>.
The protected query failure prefix is exactly:
Failed to query SecuritySignals records:
The protected row-mapping failure prefix is exactly:
Failed to map SecuritySignals row:
The primitive repository:
- catches
PDOExceptionfor query execution; - catches
Throwablefor row mapping; - preserves the original throwable as
previous.
A future mapper extraction must not alter the constructor, public signatures, filters, cursor activation, ordering, limit behavior, hydration fallbacks, catch boundaries, or message prefixes.
The exact table is:
maa_event_logging_security_signals
| Column | SQL type | Nullability / constraint |
|---|---|---|
id |
BIGINT UNSIGNED AUTO_INCREMENT |
primary key |
event_id |
CHAR(36) |
NOT NULL |
actor_type |
VARCHAR(32) |
NOT NULL |
actor_id |
BIGINT |
NULL |
signal_type |
VARCHAR(100) |
NOT NULL |
severity |
VARCHAR(16) |
NOT NULL |
correlation_id |
CHAR(36) |
NULL |
request_id |
VARCHAR(64) |
NULL |
route_name |
VARCHAR(255) |
NULL |
ip_address |
VARCHAR(45) |
NULL |
user_agent |
VARCHAR(512) |
NULL |
metadata |
JSON |
NOT NULL |
occurred_at |
DATETIME(6) |
NOT NULL |
Unique constraint:
uq_el_security_signals_event_id (event_id)
idx_el_security_signals_time
(occurred_at, id)
idx_el_security_signals_actor_time
(actor_type, actor_id, occurred_at)
idx_el_security_signals_type_time
(signal_type, occurred_at)
idx_el_security_signals_severity_time
(severity, occurred_at)
idx_el_security_signals_corr_time
(correlation_id, occurred_at)
idx_el_security_signals_request_time
(request_id, occurred_at)
actor_type, signal_type, severity, correlation_id, and request_id are leftmost indexed dimensions.
actor_id without actor_type cannot use the leftmost prefix of idx_el_security_signals_actor_time and may require a wider scan. This is an accepted performance implication, not a public API validation restriction.
The table is non-authoritative and best-effort.
SecuritySignals recording failures must not affect application control flow.
metadata must not contain passwords, OTP codes, tokens, credentials, or other secrets.
SecuritySignalsPolicyInterface belongs to the write/recording path.
It:
- normalizes actor type;
- normalizes severity;
- validates metadata JSON size.
SecuritySignalsDefaultPolicy falls back to:
ANONYMOUSfor invalid actor type;INFOfor invalid severity;- maximum metadata JSON size of
65535bytes.
SecuritySignalsRecorder catches Throwable across command construction, policy normalization, metadata processing, record DTO construction, writer execution, and fallback logging.
Recording remains fail-open.
The primitive query repository does not use SecuritySignalsPolicyInterface.
Therefore, the proposed shared read mapper is policy-free:
- no policy constructor;
- no actor-type normalization during reads;
- no severity normalization during reads;
- no metadata-size validation during reads;
- exact preservation of current persisted-row fallbacks.
Admin Query calls are direct read operations. Their validation, execution, and storage exceptions are exposed to their caller; they are not converted into recorder-style fail-open behavior.
PR #74 introduced exactly these Runtime artifacts:
src/SecuritySignals/Contract/SecuritySignalsPaginatedQueryInterface.php
src/SecuritySignals/DTO/SecuritySignalsQueryCursorDTO.php
src/SecuritySignals/DTO/SecuritySignalsQueryPageDTO.php
src/SecuritySignals/Service/SecuritySignalsPaginatedQueryService.php
It introduced exactly these tests:
tests/Unit/SecuritySignals/DTO/SecuritySignalsQueryCursorDTOTest.php
tests/Unit/SecuritySignals/DTO/SecuritySignalsQueryPageDTOTest.php
tests/Unit/SecuritySignals/Service/SecuritySignalsPaginatedQueryServiceTest.php
Classification:
Superseded Post-v1 Experiment
Known package references are limited to the wrapper family itself, its tests, and architecture/history documentation:
- the service implements
SecuritySignalsPaginatedQueryInterface; - the service depends on the protected primitive query interface;
- the interface references the primitive query DTO and wrapper page DTO;
- the page DTO references the wrapper cursor DTO and primitive view DTO;
- the three unit tests directly exercise the wrapper family;
- package reference, architecture, roadmap, and compatibility documents describe the family or its remediation state.
No current default factory, provider, bootstrap binding, or package example makes this wrapper the approved Admin Query target.
The wrapper interface, service shape, constructors, page DTO, cursor DTO, serialized keys, cursor-generation approach, wrapper pagination semantics, and coupling to the primitive query path are not protected compatibility targets.
This is a public library. Maintained host repositories must be searched for imports, construction, interface implementation, service calls, DTO usage, factories, bindings, tests, and documentation references.
The known current state is that the superseded wrapper has not been fully adopted by any host project.
Any discovered host use must be migrated to the approved Admin Query API and verified in the relevant host repository.
However:
- host search and migration do not convert the superseded package artifacts into protected contracts;
- host search and migration do not postpone package-level deletion;
- the seven superseded Runtime/test artifacts are deleted in the same SecuritySignals Runtime rebuild change set that adds and verifies the replacement;
- host migration may be delivered through coordinated host-repository PRs against the package version containing the rebuilt API;
- retaining the obsolete wrapper as an active or deprecated compatibility layer requires a new explicit Owner decision.
The package-level Runtime rebuild is atomic:
- add the approved SecuritySignals Admin Query implementation;
- add its Unit, Regression, and strict real-MySQL Integration coverage;
- preserve the protected
v1.0.0primitive behavior; - delete the exact seven superseded post-v1 artifacts;
- update package and integration documentation.
PR #102 and PR #103 were documentation-only and implemented no deletion themselves.
The public interface is:
namespace Maatify\EventLogging\SecuritySignals\Contract;
use Maatify\EventLogging\SecuritySignals\DTO\SecuritySignalsAdminPageResultDTO;
use Maatify\EventLogging\SecuritySignals\DTO\SecuritySignalsAdminQueryRequestDTO;
use Maatify\EventLogging\SecuritySignals\Exception\SecuritySignalsAdminQueryExecutionException;
use Maatify\EventLogging\SecuritySignals\Exception\SecuritySignalsAdminQueryInvalidArgumentException;
use Maatify\EventLogging\SecuritySignals\Exception\SecuritySignalsStorageException;
interface SecuritySignalsAdminQueryInterface
{
/**
* @throws SecuritySignalsAdminQueryInvalidArgumentException
* @throws SecuritySignalsAdminQueryExecutionException
* @throws SecuritySignalsStorageException
*/
public function paginate(
SecuritySignalsAdminQueryRequestDTO $request,
): SecuritySignalsAdminPageResultDTO;
}The method name is paginate().
No maatify/persistence class appears in the public interface.
The request DTO is:
namespace Maatify\EventLogging\SecuritySignals\DTO;
use DateTimeImmutable;
use JsonSerializable;
final readonly class SecuritySignalsAdminQueryRequestDTO implements JsonSerializable
{
public ?string $actorType;
public ?int $actorId;
public ?string $signalType;
public ?string $severity;
public ?string $requestId;
public ?string $correlationId;
public ?DateTimeImmutable $after;
public ?DateTimeImmutable $before;
public int|string|null $page;
public int|string|null $perPage;
public ?string $sortBy;
public ?string $sortDirection;
public function __construct(
?string $actorType = null,
?int $actorId = null,
?string $signalType = null,
?string $severity = null,
?string $requestId = null,
?string $correlationId = null,
?DateTimeImmutable $after = null,
?DateTimeImmutable $before = null,
int|string|null $page = null,
int|string|null $perPage = null,
?string $sortBy = null,
?string $sortDirection = null,
) {
// Exact rules are defined below.
}
}The exact serialized keys and order are:
actorType
actorId
signalType
severity
requestId
correlationId
after
before
page
perPage
sortBy
sortDirection
after and before serialize using DATE_ATOM.
For actorType, signalType, severity, requestId, correlationId, sortBy, and sortDirection:
nullremainsnull;- trim the string;
- an empty trimmed string becomes
null; - validate UTF-8 without requiring
ext-mbstringusing the established/./uspreg_match_allpattern; - reject values longer than the approved maximum.
Maximum lengths:
| Field | Maximum |
|---|---|
actorType |
32 |
signalType |
100 |
severity |
16 |
requestId |
64 |
correlationId |
36 |
sortBy |
64 |
sortDirection |
4 |
actorId, when non-null, must be greater than zero;- a positive
actorIdis valid whenactorTypeisnull; actorTypeis valid whenactorIdisnull;- both may be supplied together;
- neither is required.
This independent-filter contract is intentional because the package is a general-purpose library and host systems may identify actors by type and ID, type only, ID only, or neither filter.
The wider-scan risk for actorId without actorType is documented but is not converted into a validation error.
- if both dates are supplied,
aftermust be less than or equal tobefore; - equal boundaries are valid;
pageremainsint|string|null;perPageremainsint|string|null;- page normalization, per-page clamping, and offset calculation are delegated to
maatify/persistence.
Public caller-selectable sorting is limited to:
occurred_at
Rules:
sortBy = occurred_at -> occurred_at
sortBy = id -> null
other short value -> null
overlong value -> invalidLength(sortBy)
invalid UTF-8 -> invalidEncoding(sortBy)
sortDirection = asc -> ASC
sortDirection = desc -> DESC
other short value -> null
overlong value -> invalidLength(sortDirection)
invalid UTF-8 -> invalidEncoding(sortDirection)
id is an internal deterministic tie-breaker only.
The result DTO is:
namespace Maatify\EventLogging\SecuritySignals\DTO;
use ArrayIterator;
use IteratorAggregate;
use JsonSerializable;
/**
* @implements IteratorAggregate<int, SecuritySignalsViewDTO>
*/
final readonly class SecuritySignalsAdminPageResultDTO implements IteratorAggregate, JsonSerializable
{
/**
* @param list<SecuritySignalsViewDTO> $items
*/
public function __construct(
public array $items,
public int $page,
public int $perPage,
public int $total,
public int $filtered,
public int $totalPages,
public bool $hasNext,
public bool $hasPrevious,
public string $sortBy,
public string $sortDirection,
) {
}
/**
* @return ArrayIterator<int, SecuritySignalsViewDTO>
*/
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->items);
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return [
'items' => $this->items,
'page' => $this->page,
'perPage' => $this->perPage,
'total' => $this->total,
'filtered' => $this->filtered,
'totalPages' => $this->totalPages,
'hasNext' => $this->hasNext,
'hasPrevious' => $this->hasPrevious,
'sortBy' => $this->sortBy,
'sortDirection' => $this->sortDirection,
];
}
}There is no root-level id field.
src/SecuritySignals/Infrastructure/Mysql/SecuritySignalsAdminQueryMysqlRepository.php
It is a public package infrastructure adapter implementing SecuritySignalsAdminQueryInterface.
Approved constructor:
public function __construct(private PDO $pdo)The constructor accepts only PDO.
The mapper, descriptor builder, paginator, sort whitelist, and pagination configuration are created internally.
No policy, injectable paginator, or production testing seam is authorized as a public constructor parameter.
src/SecuritySignals/Infrastructure/Mysql/SecuritySignalsRowMapper.php
src/SecuritySignals/Infrastructure/Mysql/Pagination/
SecuritySignalsAdminQueryDescriptorBuilder.php
The row mapper and descriptor builder are marked @internal.
The Admin repository is not marked @internal.
The Admin repository:
- creates
PageRequestfrom request pagination fields; - builds the SecuritySignals descriptor;
- creates canonical pagination configuration;
- invokes
PdoPaginator; - maps each data row through
SecuritySignalsRowMapper; - returns
SecuritySignalsAdminPageResultDTO; - owns no transaction.
Approved mapper contract:
/** @internal */
final class SecuritySignalsRowMapper
{
/**
* @param array<string, mixed> $row
*/
public function map(array $row): SecuritySignalsViewDTO
{
// Must reproduce the protected primitive mapping exactly.
}
}The mapper:
- has no policy constructor;
- applies no read-time actor-type normalization;
- applies no read-time severity normalization;
- applies no read-time metadata-size validation;
- preserves every primitive fallback listed in Section 2.6;
- throws on invalid persisted date text exactly as the current primitive mapping does.
A primitive refactor may delegate row construction to this mapper only after regression tests prove identical behavior.
The repository translates mapper failures using:
Failed to map SecuritySignals row: {original message}
A SecuritySignalsStorageException already produced inside the mapping callback must propagate unchanged and must not be rewrapped as a pagination execution failure.
The descriptor builder generates one shared structure:
array{
whereSql: string,
params: array<string, string|int|bool|null>
}The same whereSql and parameters are used by filtered-count and data SQL.
Parameter-array keys do not contain leading colons.
Accepted filters map exactly as follows:
| Request field | SQL |
|---|---|
actorType |
actor_type = :actor_type |
actorId |
actor_id = :actor_id |
signalType |
signal_type = :signal_type |
severity |
severity = :severity |
requestId |
request_id = :request_id |
correlationId |
correlation_id = :correlation_id |
after |
occurred_at >= :after |
before |
occurred_at <= :before |
Dates are converted to UTC and formatted using:
Y-m-d H:i:s.u
DATE_ATOM is used only by DTO JSON serialization.
SELECT COUNT(*)
FROM maa_event_logging_security_signalsThere are no package-owned mandatory row-visibility constraints for SecuritySignals. Therefore, total counts all rows.
SELECT COUNT(*)
FROM maa_event_logging_security_signals
{whereSql}SELECT
id,
event_id,
actor_type,
actor_id,
signal_type,
severity,
correlation_id,
request_id,
route_name,
ip_address,
user_agent,
metadata,
occurred_at
FROM maa_event_logging_security_signals
{whereSql}The Admin Query descriptor uses an explicit 13-column selection.
It must not use SELECT *.
The descriptor data SQL must not contain:
ORDER BY
LIMIT
OFFSET
Those mechanics belong to maatify/persistence.
The Admin request does not support:
eventId
routeName
ipAddress
userAgent
metadata
free-text search
arbitrary column names
arbitrary SQL expressions
These values remain present in returned SecuritySignalsViewDTO items where applicable.
Generic page mechanics are delegated to maatify/persistence ^1.1.0.
Canonical configuration:
public sort key:
occurred_at
internal sort mapping:
occurred_at -> occurred_at
id -> id
default sort:
occurred_at DESC
tie-breaker:
id DESC
default per page:
20
minimum per page:
1
maximum per page:
200
The public EventLogging interface and DTOs expose no persistence package classes.
total, filtered, data ordering, page normalization, per-page clamping, offset calculation, LIMIT, OFFSET, and canonical pagination metadata are produced through the persistence package.
Storage execution failures:
PaginationExecutionException
PDOException
translate to SecuritySignalsStorageException using:
Failed to query SecuritySignals records: {original message}
Pagination configuration and query/descriptor validation failures:
InvalidPaginationConfigurationException
InvalidPaginationQueryException
translate to SecuritySignalsAdminQueryExecutionException.
Mapper failures translate to SecuritySignalsStorageException using:
Failed to map SecuritySignals row: {original message}
The original throwable is preserved as previous.
Storage failures must not be translated into SecuritySignalsAdminQueryExecutionException.
Approved class:
SecuritySignalsAdminQueryInvalidArgumentException
It:
- extends
Maatify\Exceptions\Exception\Validation\InvalidArgumentMaatifyException; - implements
Maatify\EventLogging\Exception\EventLoggingExceptionInterface.
Exact factories and messages:
invalidId(field)
Invalid SecuritySignals Admin Query ID: {field}
invalidLength(field)
Invalid SecuritySignals Admin Query length: {field}
invalidEncoding(field)
Invalid SecuritySignals Admin Query UTF-8 encoding: {field}
invalidDateRange()
Invalid SecuritySignals Admin Query date range: after must be before or equal to before
actorId without actorType is not an invalid-ID condition.
Approved class:
SecuritySignalsAdminQueryExecutionException
It:
- extends
SystemMaatifyException; - implements
EventLoggingExceptionInterface; - preserves the previous throwable;
- uses
ErrorCodeEnum::MAATIFY_ERROR.
Exact approved message:
SecuritySignals Admin Query execution failed: {original message}
The existing SecuritySignalsStorageException remains the storage boundary.
Its class, error code, and primitive behavior are not redesigned.
The current primitive SQL reuses the named placeholder :cursor_at twice.
Repeated named placeholders are prohibited by the project standard and are not an Owner choice.
The Runtime implementation must use:
(
occurred_at < :cursor_at_before
OR (
occurred_at = :cursor_at_equal
AND id < :cursor_id
)
)Both timestamp parameters receive the same UTC-formatted value.
This required correction:
- does not change the primitive public interface;
- does not change cursor activation;
- does not change ordering;
- does not change limit behavior;
- does not change page semantics;
- does not change returned records;
- must be delivered with focused unit coverage;
- must be delivered with primitive regression coverage;
- must be proven using real MySQL with native prepared statements.
PR #102 documented the requirement but did not implement it.
- exact constructor field order and defaults;
- exact serialized key order;
DATE_ATOMserialization;- nullable values;
- trimming;
- empty-string normalization;
- UTF-8 validation without
ext-mbstring; - every maximum-length boundary;
- every over-limit case;
- positive
actorId; - zero
actorIdrejection; - negative
actorIdrejection; actorIdwithoutactorTypeaccepted;actorTypewithoutactorIdaccepted;- both actor filters accepted together;
- neither actor filter accepted;
- equal date boundaries;
- invalid date order;
- page/per-page passthrough;
- valid public sort;
- invalid short sort normalization;
- overlong sort rejection;
- invalid UTF-8 sort rejection;
- case-insensitive direction normalization.
- constructor values;
- exact serialized root keys;
- exact serialized key order;
- iterator behavior;
- empty items;
- canonical pagination metadata;
- no root-level
id.
- every field mapped normally;
- numeric
idfallback; event_idfallback;- nullable
actor_typefallback; - numeric
actor_idfallback; signal_typefallback;severityfallback;- nullable context fields;
- missing/non-string date fallback;
- invalid date failure;
- associative metadata;
- missing metadata;
- non-string metadata;
- empty metadata;
- malformed JSON;
- scalar JSON;
- numeric-key JSON array;
- mapper has no policy dependency.
- total SQL exactly matches the table-only count;
- filtered SQL uses the generated
WHERE; - data SQL uses the explicit 13-column list;
- no
SELECT *; - no
ORDER BY,LIMIT, orOFFSETin descriptor SQL; - every filter individually;
actorIdfilter withoutactorType;actorTypefilter withoutactorId;- actor pair together;
- combined non-actor filters;
- all filters together;
- exact parameter names;
- parameter keys without leading colons;
- exact parameter values;
- UTC date conversion;
Y-m-d H:i:s.uSQL formatting;- inclusive date boundaries;
- equal date boundaries;
- filtered-count/data
WHEREidentity; - filtered-count/data parameter identity;
- public sort whitelist behavior;
- internal tie-breaker mapping;
- default and tie-breaker directions;
- default/minimum/maximum per-page configuration.
PDO-only constructor;- page request creation;
- paginator result mapping;
- result DTO mapping;
- no transaction ownership;
- PDO failure storage translation;
- pagination execution storage translation;
- invalid pagination configuration execution translation;
- invalid pagination query execution translation;
- row-mapping prefix;
- query prefix;
- previous throwable preservation;
- mapper storage exception propagation without rewrapping.
SecuritySignalsQueryInterface::find()signature;- primitive DTO constructor order;
- primitive DTO defaults;
- primitive DTO serialization;
- primitive view DTO constructor;
- primitive view DTO serialization;
- primitive repository
PDO-only constructor; - every primitive filter;
- independent primitive actor filters;
- cursor condition only when both cursor values exist;
- distinct cursor timestamp placeholders;
- both timestamp parameters receive the same value;
occurred_at DESC, id DESC;max(1, limit);- non-array row skipping;
- all hydration fallbacks;
- all metadata JSON behaviors;
- exact query prefix;
- exact mapping prefix;
- previous throwable preservation;
- no transaction ownership;
- write-side policy behavior unchanged;
- recorder fail-open behavior unchanged;
- exact seven superseded wrapper/test artifacts are absent from the completed Runtime rebuild;
- no package-owned Runtime reference to a deleted wrapper artifact remains;
- no regression test treats wrapper page/cursor contracts or cursor-wrapper mechanics as protected behavior.
- live MySQL only;
- no SQLite fallback;
- native prepared statements;
- missing DSN fails the strict gate;
- connection failure fails the strict gate;
- no silent integration skip reported as success;
- every Admin filter separately;
actorIdwithoutactorType;actorTypewithoutactorId;- actor pair together;
- combined filters;
- inclusive
afterandbefore; - equal date boundaries;
- zero rows;
totalversusfiltered;- multiple pages;
- page normalization;
- per-page clamping;
- same-timestamp deterministic ordering by
id DESC; - nullable fields;
- metadata hydration;
- primitive distinct-placeholder correction;
- primitive/Admin semantic alignment;
- repository does not own the caller transaction.
composer validate --strict
composer analyse
composer test:unit
composer test:regression
real MySQL integration suite
git diff --check
A skipped Integration job is not a passing Integration result.
Runtime:
src/SecuritySignals/Contract/SecuritySignalsAdminQueryInterface.php
src/SecuritySignals/DTO/SecuritySignalsAdminQueryRequestDTO.php
src/SecuritySignals/DTO/SecuritySignalsAdminPageResultDTO.php
src/SecuritySignals/Exception/SecuritySignalsAdminQueryInvalidArgumentException.php
src/SecuritySignals/Exception/SecuritySignalsAdminQueryExecutionException.php
src/SecuritySignals/Infrastructure/Mysql/SecuritySignalsAdminQueryMysqlRepository.php
src/SecuritySignals/Infrastructure/Mysql/SecuritySignalsRowMapper.php
src/SecuritySignals/Infrastructure/Mysql/Pagination/SecuritySignalsAdminQueryDescriptorBuilder.php
Tests:
tests/Unit/SecuritySignals/DTO/SecuritySignalsAdminQueryRequestDTOTest.php
tests/Unit/SecuritySignals/DTO/SecuritySignalsAdminPageResultDTOTest.php
tests/Unit/SecuritySignals/Exception/SecuritySignalsAdminQueryInvalidArgumentExceptionTest.php
tests/Unit/SecuritySignals/Exception/SecuritySignalsAdminQueryExecutionExceptionTest.php
tests/Unit/SecuritySignals/Infrastructure/Mysql/SecuritySignalsAdminQueryMysqlRepositoryTest.php
tests/Unit/SecuritySignals/Infrastructure/Mysql/SecuritySignalsRowMapperTest.php
tests/Unit/SecuritySignals/Infrastructure/Mysql/Pagination/SecuritySignalsAdminQueryDescriptorBuilderTest.php
tests/Regression/SecuritySignals/SecuritySignalsQueryMysqlRepositoryRegressionTest.php
tests/Integration/SecuritySignals/SecuritySignalsAdminQueryMysqlRepositoryTest.php
tests/Integration/SecuritySignals/SecuritySignalsQueryMysqlRepositoryTest.php
src/SecuritySignals/Infrastructure/Mysql/SecuritySignalsQueryMysqlRepository.php
src/SecuritySignals/README.md
EVENT_LOGGING_PACKAGE_REFERENCE.md
docs/integration/ADMIN_READ_USAGE.md
docs/architecture/ADMIN_QUERY_SECURITY_SIGNALS_REBUILD_BLUEPRINT.md
docs/roadmap/ADMIN_QUERY_API_ROADMAP.md
docs/audits/DOCUMENTATION_INVENTORY.md
CHANGELOG.md
The primitive repository modification is restricted to:
- shared mapper extraction with exact behavior preservation;
- mandatory distinct cursor timestamp placeholders;
- complete regression and strict real-MySQL coverage.
src/SecuritySignals/Contract/SecuritySignalsPaginatedQueryInterface.php
src/SecuritySignals/DTO/SecuritySignalsQueryCursorDTO.php
src/SecuritySignals/DTO/SecuritySignalsQueryPageDTO.php
src/SecuritySignals/Service/SecuritySignalsPaginatedQueryService.php
tests/Unit/SecuritySignals/DTO/SecuritySignalsQueryCursorDTOTest.php
tests/Unit/SecuritySignals/DTO/SecuritySignalsQueryPageDTOTest.php
tests/Unit/SecuritySignals/Service/SecuritySignalsPaginatedQueryServiceTest.php
The Runtime rebuild must contain both the approved replacement and these exact deletions.
Package-level completion requires:
- replacement Runtime is complete;
- package unit tests pass;
- package regression tests pass;
- strict real MySQL integration tests pass;
- static analysis passes;
- Composer validation passes;
- no package Runtime or active documentation reference presents a deleted artifact as usable API.
Maintained host repositories are searched and any discovered usage is migrated as coordinated integration work. That host work does not preserve the obsolete package API and does not defer the package-level deletion to a later cleanup phase.
- primitive public interface and method signature;
- primitive DTO public constructors and serialization;
- primitive repository
PDO-only constructor; - primitive filters;
- cursor activation rule;
- ordering;
- limit behavior;
- row hydration;
- storage exception class and prefixes;
- schema;
- writer behavior;
- policy behavior;
- recorder fail-open boundary;
- existing factory/provider/bootstrap behavior unless separately approved.
schema changes
Composer dependency changes
CI workflow changes
HTTP controllers
routes
middleware
permissions
authentication
UI
localization
exports
dashboards
reporting
free-text search
cross-domain generic repositories
host application wiring inside this package PR
tagging
release publication
The following Owner decisions are final and the complete blueprint is approved as one coherent Runtime contract.
actorType + actorId: valid
actorType only: valid
actorId only: valid
neither: valid
A positive actorId must not be rejected because actorType is null.
The index implication is documented as a performance consideration only.
The primitive repeated :cursor_at placeholder must be replaced by:
:cursor_at_before
:cursor_at_equal
This is required by the project standard and is not an optional Owner choice.
The exact seven superseded artifacts are outside the protected v1.0.0 contract.
They must be deleted inside the same Runtime rebuild change set that adds and verifies the replacement Admin Query API.
Maintained host repositories must be searched and every discovered use migrated, but:
- host search does not protect the superseded wrapper;
- host migration does not postpone package-level deletion;
- wrapper page/cursor contracts, serialization, and cursor-wrapper mechanics are not compatibility targets;
- retaining the obsolete package API requires a new explicit Owner decision.
The complete package contract has been reviewed and approved as one coherent blueprint, including:
- public
paginate()interface; - request DTO and normalization contract;
- result DTO contract;
- policy-free mapper;
- public repository path and
PDO-only constructor; - descriptor and SQL contract;
- pagination configuration;
- exception names and messages;
- exact Runtime file inventory;
- test, atomic-retirement, and host-integration gates.
This approval authorizes a separate Runtime implementation task/PR. It does not authorize deviation from this contract, a tag, or a release.
PR #102 and PR #103 are merged documentation history. They implemented no SecuritySignals Runtime code or artifact deletion.
The implementation occurred across the following sequence:
907b24b: added the initial Runtime implementation.2b4003b: expanded the Runtime coverage and updated the documentation, but later review identified remaining exception-boundary gaps.bcadba8: added the missing exception-boundary tests but opened unauthorized production seams.4cfb536: removed those seams and established the final accepted Runtime state.
The Runtime change:
- implements the exact approved Admin Query public and internal contracts in this blueprint;
- preserves every protected
v1.0.0primitive, schema, write-policy, and fail-open behavior; - applies the behavior-preserving native-PDO distinct-placeholder correction;
- adds the complete Unit, Regression, and strict real-MySQL Integration coverage defined here;
- adds the approved replacement and deletes the exact seven superseded Runtime/test artifacts inside the same Runtime rebuild change set;
- searches maintained host repositories and records no discovered wrapper usage;
- updates the Package Reference, SecuritySignals README, Admin integration documentation, roadmap, documentation inventory, and changelog as applicable;
- passes Composer validation, PHPStan, Unit, Regression, strict real-MySQL Integration, documentation, architecture-boundary, and
git diff --checkgates.
No tag, release publication, reporting work, dashboard work, schema change, Composer dependency change, CI workflow change, or host framework wiring is authorized by this approval.
Current status:
Owner Approved / Runtime Implemented