* chore(triage): pass-1 closes 106 issues, scaffolds 7 missions; pass-2 decomposes architectural-remediation
Pass 1 architect-mode triage (2026-04-30, modern stance: PHP 8.4+,
no legacy, breaking changes welcome):
- Closed 106 issues across 7 criteria with cross-links to absorbing
Spec Kitty missions
- Backlog 122 -> 16 (anchor + user-flagged keeps + pending verification)
- Methodology and full kill list:
docs/triage/2026-04-30-pass-1-kill-list.md
Pass 2 scaffolded 7 missions absorbing the closed work:
- 824-architectural-remediation (52 children, fully decomposed)
- 1335-layer-coverage-completion (9)
- 619-agentic-framework-organs (4)
- 1257-entity-storage-hardening (8)
- 1107-api-symfony-decoupling (1)
- 589-parity-feature-set (13)
- 584-perf-n-plus-one (5)
WP01 decomposition for architectural-remediation: 8 contract surfaces
(S1-S8), 8 work packages (WP02-WP09) with strict dependency-ordered
sequencing, NO-SPLIT decision (one tightly coupled remediation graph).
Two new framework contracts ratified:
- KernelServicesInterface: typed kernel-services bus replacing the
Closure-based kernelResolver fallback in ServiceProvider
- composer verify: canonical repo-wide verification command bundling
CS, PHPStan, layer checks, composer policy, no-secrets,
ingestion-defaults, manifest, and contract tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(#1335): WP01 spec-lock for layer-coverage-completion mission
Decomposes 1335-layer-coverage-completion into 9 WPs (per-layer L0-L6 coverage
sweeps + phpstan-neon/audit-tool modernization + CI gate). Mechanical pattern,
no architectural contract surfaces.
Three conventions ratified inline:
- C1: #[CoversClass] attribute over @covers PHPDoc (Path A) — modernizes
tools/audit/GenerateLayerAudit.php to PHPUnit 10.5+ idiom
- C2: @internal as the sole exemption mechanism
- C3: PHPStan neon exclusion documented inline per excluded package
Drift flag carried into WP09: #1342 claimed 7 missing L6 packages; live
phpstan.neon (2026-04-30) shows only 2 (admin intentional, deployer real).
Use live file as ground truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(#619): WP01 spec-lock for agentic-framework-organs mission
Decomposes 619-agentic-framework-organs into 6 WPs (WP02 spec-lock + RFC
import, WP03/WP04 organ scaffolds in parallel, WP05 brainstem extensions,
WP06 wiring, WP07 layer-discipline gate). Architectural mode (matches 824
pattern), NO-SPLIT decision recorded.
10 contracts ratified inline in three batches:
- Memory (C1-C3): MemoryStoreInterface, MemoryRetrieverInterface,
ReweaveProcessorInterface
- Guardrails (C4-C7): GuardrailPolicyInterface, ToolPermissionPolicyInterface
+ tier enum, Pre/PostExecutionHookInterface, EscalationHandlerInterface
- Kernel (C8-C10): PlannerInterface + PlanStep, AgentRegistryInterface +
OrchestratorInterface, AgentExecutorInterface (shared with #1242)
D1 resolved Path A: GuardrailPolicyInterface lives at L5 in ai-guardrails;
Bimaaji L4 GuardrailRule value object stays unchanged; new L5 class
Anishinaabe\SovereigntyGuardrailPolicy implements the interface and consumes
Bimaaji data via DI. No L4->L5 import edge introduced. Honors layer rule
and shape distinction (data vs behavior).
D5 carried into WP02: copy claudriel/docs/specs/waaseyaa-agentic-framework-rfc.md
into Waaseyaa docs/specs/ai-memory.md and docs/specs/ai-guardrails.md before
WP03+ run. Framework becomes source of truth.
Drift flags D2/D3/D4/D6 resolved in spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(#1257): WP01 spec-lock for entity-storage-hardening mission
Decomposes 1257-entity-storage-hardening into 10 WPs (WP02 spec execution,
WP03-WP10 hardening fixes, WP11 kernel-path integration test as the lock).
Mode: mechanical-with-one-architectural-asterisk. NO-SPLIT.
7 conventions ratified inline (K1-K7):
- K1 bundle subtable naming via single helper + registration-time guard
- K2 read routing matches write routing for FieldStorage::Data
- K3 _data JSON value comparisons coerce by declared field type
- K4 diagnostic-loop tightening (log once per (entity_type, bundle), do not throw)
- K5 dialect-portable diagnostic enumeration (DBAL listTableNames, SQLite fast-path)
- K6 HealthChecker layer placement: option (c) codified kernel-adjacent
exemption in bin/check-package-layers. Hard prerequisite: WP02 verifies
824 S1 exemption surface has merged; if not, mission blocks on 824.
- K7 duplicate-registration error names both registrants
1 architectural contract ratified inline (C1): tenancy opt-in declared on
EntityType via 'tenancy' => ['scope' => 'community'] constructor key (Option 1).
Deprecates HasCommunityInterface marker with log-once-per-entity-type cadence.
Breaking change with deprecation cycle, approved under modern stance.
Open-issue handling: Path X. WP02 closes #1298, #1299, #1300, #1301, #1304,
#1308, #1313 with cross-link comments. Anchor #1257 stays open per user flag;
body annotated with merged-commit refs at WP11 acceptance.
8 drift flags resolved in spec. WP11 (kernel-path integration test) is the
charter's stated lock and non-negotiable for mission acceptance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(#1107): WP01 spec-lock for api-symfony-decoupling mission
Decomposes 1107-api-symfony-decoupling into 4 WPs (WP02 Foundation Request
type, WP03 EventDispatcher interface, WP04 JsonApiResponse + trait
consolidation, WP05 spec docs + contract test). Mode: architectural,
NO-SPLIT, single-issue mission (no absorbed children beyond the anchor).
5 contracts ratified inline (C1-C5):
- C1 (a) JsonApiResponse subclasses Symfony JsonResponse for v1; future major
may flip to standalone wrapper
- C2 (a) Foundation\Http\Request as class_alias of Symfony Request; real
wrapper deferred to multi-mission migration
- C3 (a) DomainEvent keeps Symfony Event parent; only the dispatcher gets a
Waaseyaa-owned interface; framework-internal leak documented in
docs/specs/infrastructure.md
- C4 (a) JsonApiResponseTrait moves to api package with foundation-side
deprecation shim. Hard prerequisite: WP02 verifies a clean shim path
exists without violating L0 -> L4 layer rule. If not, surface to user.
- C5 (b) Symfony-import boundary linter deferred to follow-up mission;
WP05 files the issue at acceptance
Charter-vs-body framing resolved Path R-narrow: mission solves request /
response / event-dispatch only. Routing stays Symfony-coupled. WP05 files
follow-up routing-symfony-decoupling mission.
WP06 dropped (was conditional on C5 (a)).
7 drift flags resolved in spec. Single-issue mission status acknowledged
(meta.json child_issues contains only the anchor).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(#589): WP01 spec-lock for parity-feature-set mission
Decomposes 589-parity-feature-set into 7 WPs (WP02 verification matrix +
WP03-WP07 parallel reconciliation). Mode: mechanical-with-architectural-asterisk.
NO-SPLIT.
Mission shape: reconciliation, not design. Anchor #589 and all 13 absorbed
issues are CLOSED. Live-source disposition: 8 DONE (need spec docs only),
3 PARTIAL (real fills needed), 2 GAP (deferral with follow-up issues).
10 conventions + 1 contract ratified inline:
- K1 verification-before-build discipline (live source is ground truth)
- K2 scheduler spec authored in docs/specs/infrastructure.md
- K3 notification spec authored in docs/specs/infrastructure.md
- K4 OAuth provider spec authored in new docs/specs/oauth.md or expanded
docs/specs/access-control.md
- K5 OIDC server spec authored in new docs/specs/oidc.md
- K6 workflows spec authored in new docs/specs/workflows.md
- K7 (a) UserBlock ships in waaseyaa/messaging package (BlockAccessPolicy
included). No new social package.
- K8 (a) EntityFactory base in packages/testing; fakerphp/faker as
require-dev of testing; bin/waaseyaa db:seed in packages/cli with soft
dependency on testing.
- K9 (b) Form API deferred. WP07 documents in new docs/specs/form-api.md
and files a new follow-up issue. Closed #594 stays closed.
- K10 (b) Framework webhook surface deferred. WP07 documents in
docs/specs/infrastructure.md and files a new follow-up issue. Closed
#628 stays closed. Billing-specific WebhookHandler stays as-is.
- C1 RedisCacheBackend + MemcachedCacheBackend implementing
CacheBackendInterface + TagAwareCacheInterface, wired via CacheConfigResolver.
10 drift flags resolved in spec. Anchor closed-state acknowledged. Eleven
of thirteen issues already shipped per live-source audit; verification
matrix (WP02) is hard precondition for any code WP to prevent rebuilding
existing packages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(#584): WP01 spec-lock for perf-n-plus-one mission
Decomposes 584-perf-n-plus-one into 6 WPs (WP02 verification + WP03 identity
map + WP04 includes batcher + WP05 relationship pagination + WP06 DataLoader
verification). Mode: mechanical-with-architectural-asterisk. NO-SPLIT.
Mission shape: reconciliation + targeted fills. All 5 absorbed issues are
CLOSED. Live-source disposition: 1 DONE (#587 ReferenceLoader already ships),
1 PARTIAL/MIS-FILED (#586 wrong file - identity map belongs on
SqlEntityStorage not SqlEntityQuery), 3 GAP (#584/#585/#588 real fills).
6 conventions ratified inline (K1-K6):
- K1 verification-before-build discipline
- K2 identity map lives on SqlEntityStorage, not SqlEntityQuery (re-targets
#586; load-bearing boundary preventing cross-mission collision with #1257)
- K3 pagination total via SQL COUNT(*), not array count
- K4 ?include= as two-pass collect-then-batch-load (pattern reuse from
ReferenceLoader, no class sharing)
- K5 (a) eager-load returns EntityIncludeBundle wrapper; preserves EntityBase
immutability; in-place hydration via setLoadedReference rejected
- K6 (a) cluster-aggregation partial fix; SQL LIMIT/OFFSET on edge-list paths
only; cluster path keeps in-PHP aggregation with documented limitation;
GROUP BY rewrite deferred to future work
3 contracts ratified inline (C1-C3):
- C1 EntityRepository::find/findMany/findBy gain ?array $include = null with
union return type (EntityInterface | EntityIncludeBundle); BC preserved
when $include === null
- C2 new Waaseyaa\Api\IncludeBatcher class with paired-nullable
EntityAccessHandler + AccountInterface
- C3 SqlEntityStorage per-instance identity map (private, internal); two
load() calls in same request return === instance
Cross-mission sequencing: Path Q1 ratified. WP03 holds until #1257 mission
merges. K2 boundary prevents file-level collision regardless.
5 drift flags resolved in spec, including the standout #587-already-DONE
and #586-mis-filed surprises that would have caused agent collisions
without WP02's verification matrix gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: apply spec-kitty upgrade changes (3.1.6 -> 3.1.7)
* feat(#824): KernelServicesInterface replaces kernelResolver Closure (WP02 surface A)
Replace the legacy `\Closure(string): ?object` kernelResolver with a typed
KernelServicesInterface. ServiceProvider::resolve() falls back to the bus
through `setKernelServices()`, and mergeChildProvider propagates the same
instance. Default backed by ProviderRegistryKernelServices, which resolves
EntityTypeManager, DatabaseInterface, EventDispatcherInterface, LoggerInterface,
\PDO, plus sibling-provider bindings via a live providers-list accessor.
Ratifies Contract A in docs/specs/infrastructure.md. Adds the interface to
the public surface map. Migrates two existing ServiceProviderResolveTest
cases to the new API and adds a focused contract test.
Other WP02 surfaces (EventListenerRegistrar, layer-graph exemption,
admin reclassify, extension-compatibility-matrix ratification) land in
follow-up commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#824): scope kernel exemption to named files (WP02 surface C)
bin/check-package-layers now performs file-level layer enforcement in
addition to its existing composer.json `require` check. Every package's
src/**/*.php is scanned for `use Waaseyaa\X\…` imports; any import whose
target sits above the importing package's layer fails as PL005.
The kernel exemption is codified through two complementary tiers:
1. KERNEL_EXEMPT_DIR_SUFFIXES — implicit dir-prefix exemption for files
under <pkg>/src/Kernel/, the canonical kernel/bootstrapper boundary
from CLAUDE.md "Layer Architecture > Exemption".
2. KERNEL_EXEMPT_FILES — explicit named-file allowlist mapping
path → one-line rationale, for kernel-adjacent files that must live
outside Kernel/ (route registrars wired only from HttpKernel,
diagnostics wired only from ConsoleKernel, listeners wired by a
ServiceProvider).
The allowlist is pre-populated with the live set of justified
violators: foundation Diagnostic/* (HealthChecker + companion),
foundation Http/Router/* (eight registrars), and cache CacheConfigResolver
plus three event-listener wiring files. Each entry carries a rationale
so future drift cannot sneak in unjustified.
This is the prerequisite surface for mission #1257 K6(c): the
HealthChecker entry is named here so 1257 WP02 / #1300 can verify
the codified exemption without doing the relocation.
docs/specs/infrastructure.md gains a new "Kernel exemption surface
(named files)" subsection documenting both tiers and the consumer
mission.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#824): name EventListenerRegistrar in kernel exemption list (WP02 surface B)
EventListenerRegistrar lives under packages/foundation/src/Kernel/ and is
already covered by the implicit /src/Kernel/ dir exemption added by
surface C. This commit also names it explicitly in KERNEL_EXEMPT_FILES
so the architectural intent is visible in the allowlist alongside the
HealthChecker / Http/Router entries.
Decomposition (per-layer registrars in their respective packages) was
considered and rejected: the class has a single caller (HttpKernel), no
business logic — only $dispatcher->addListener(...) wiring around event
class references — and no growth pressure. Splitting it across 4 packages
would not change which symbols cross the kernel boundary; HttpKernel
would still need to invoke each fragment.
Closes the last kernel-adjacent ambiguity in the layer graph: every
cross-layer importer in foundation/cache is now either inside a Kernel/
subtree or has an explicit entry with rationale.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#824): drop admin from PHP layer graph (WP02 surface D)
packages/admin/ is a Nuxt SPA: no composer.json, zero PHP source under
the tree (only app/, contracts/, e2e/, nuxt.config.ts, package.json,
playwright.config.ts, public/, tests/, tsconfig*.json, vitest.config.ts,
README.md), and no first-party Composer manifest requires waaseyaa/admin.
The L6 PHP host extension is waaseyaa/admin-surface, which remains in the
layer table.
Reconciliation:
- Remove "admin": 6 from LAYER_BY_SHORT in bin/check-package-layers
(the entry was dead data — the iteration already skips packages without
composer.json, so the mapping was never consulted) and add a comment
explaining the SPA/host split at the L6 block.
- Drop "admin" from the L6 row of CLAUDE.md "Layer Architecture" so the
table matches the live composer graph.
- Correct the project-structure line: 62 PHP packages (verified by
counting composer.json files of type != metapackage), 3 metapackages,
1 JS admin SPA.
- Add a one-line note to docs/specs/infrastructure.md "Composer layer
graph" subsection clarifying that admin is the SPA and admin-surface
is the PHP host.
Gate impact: zero. Removing a never-consulted mapping does not change
which packages get scanned. The gate stays green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#824): add Package Layer Table to compatibility matrix (WP02 surface E)
Mission #824 WP02 acceptance evidence requires that
docs/specs/extension-compatibility-matrix.md "matches actual composer.json
graph" and acts as the canonical source of truth that gates
bin/check-package-layers. The file as it stood (27 lines, runtime contract
surfaces only) had no layer table at all — the mission spec was written
against a mental model the document never satisfied.
Surface E reconciles this by appending a Package Layer Table section that
mirrors LAYER_BY_SHORT in bin/check-package-layers row-for-row:
- 62 first-party PHP packages, one row each (Package | Layer | Notes)
- Layer split: 18 / 9 / 10 / 7 / 3 / 5 / 10 across L0–L6
- Footnotes: admin SPA exclusion, metapackages, kernel exemption surface
- Synchronization section listing the three canonical sources
(script, this table, CLAUDE.md) and noting the three must agree —
gate ratifies via PL002
Cross-check confirmed before commit: spec table and live LAYER_BY_SHORT
have identical key set and identical layer values; zero drift.
Closes WP02 (surfaces A–E):
A — KernelServicesInterface replaces kernelResolver Closure
B — EventListenerRegistrar named in kernel exemption list
C — file-level layer scanner + named-file exemption surface
D — admin dropped from PHP layer graph (Nuxt SPA)
E — Package Layer Table ratified in compatibility matrix
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#1335): WP09 phpstan-neon completeness + audit-tool modernization
Three of the five WP09 scope items required code changes; two were no-ops
verified against the live tree.
Code changes:
1. tools/audit/GenerateLayerAudit.php — extend buildSymbolTestMap() to
index #[CoversClass(...)] attributes alongside @covers PHPDoc per
Convention C1 (PHPUnit 10.5+ idiom; CLAUDE.md attribute convention).
Resolves short class refs via the test file's local use statements;
same-namespace refs without an alias are skipped (rare in this tree).
Updates methodology string and human-facing markdown report wording.
The internal JSON keys (at_covers_hits,
public_non_internal_symbols_lacking_at_covers) are kept unchanged so
the three segment_l{1,2,3}_covers_by_package.php consumers still work.
2. phpstan.neon — add inline exclusion comments per Convention C3:
- admin: Nuxt SPA, PHP host is admin-surface (L6)
- deployer: recipe-only package (no PSR-4 src/); the 103-line
recipe/waaseyaa.php depends on Deployer's globally-bound functions
(run/task/desc/get/after) that PHPStan cannot resolve without
project-specific stubs. The mission spec characterized deployer as
"real omission" assuming a typical layout; reality required C3
treatment, not inclusion.
3. build/layer{0..6}-audit/* — full artifact regeneration on the new
C1 baseline. Aggregate impact across the 7 layers: 703 covered FQCNs
(was visible: ~111), 309 missing (was: 530+). The +592 covered FQCNs
are pre-existing #[CoversClass] attributes the old audit was blind to.
WP02-WP08 inherit this baseline and drive missing→0 per layer.
No-ops verified:
4. C2 (@internal as sole exemption): already implemented at line 153
via `'internal' => str_contains($classDoc, '@internal')`. The audit
operates at class FQCN granularity, so class-level @internal is the
right scope. Method-level @internal is not relevant.
5. cast.useless in AnomalyDetector.php (decomposition cited line 136):
live cast is at line 119 (`(int) floor(count($costs) / 2)`); PHPStan
does not flag it under the current ruleset; not in baseline; not
silenced. Issue body claim was stale. No fix needed.
Drift notes (vs decomposition.md / issue bodies):
- #1342 listed 7 missing L6 packages; live file showed 2 (admin,
deployer); both now have inline rationales.
- #1340 cited cast.useless that no longer fires.
Gates: bin/check-package-layers OK; bin/check-composer-policy OK;
composer cs-check clean; composer phpstan 1044/1044 [OK] No errors;
PHPUnit Unit 5825 tests, Integration 683 tests (no regression).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(#1257): WP02 sub-task 3 — bless K1-K7 + C1 in entity/bundle specs
Migrates the K1-K7 conventions and C1 tenancy contract ratified in
.kittify/missions/1257-entity-storage-hardening/spec.md (2026-04-30) into
the canonical specs that the framework reads as authoritative.
docs/specs/entity-system.md (~120 lines added across 6 sites):
- New spec-reviewed line dated 2026-04-30.
- EntityType constructor example + parameter list gain `tenancy: ?array`
(C1) — declarative opt-in for community scoping; null = non-tenant.
- EntityTypeManagerInterface section gains paragraphs for K7 (duplicate
registration names both registrants, already true per D2 verification),
K1 (`__`-in-bundle-id structural guard at addBundleFields), and K4
part A (MISSING_BUNDLE_SUBTABLE notice at registration).
- Casting & hydration section gains "Query-builder boundary (K3)"
subsection — extends ST-9 to SqlEntityQuery::condition() so _data
JSON value comparisons coerce by declared field cast.
- SqlEntityQuery substrate gains "Read-write routing parity (K2)" —
routeFields() consults registry hint, matches write-side
splitForStorage(); no silent dual-source.
- Community Scoping section restructured: wiring paragraph now consults
EntityType::getTenancy(); new "Tenancy declaration (C1)" subsection
documents the canonical shape; new "Migration: HasCommunityInterface
-> declarative tenancy" subsection points consumers at the groups
CHANGELOG and prescribes the Minoo-specific transition order
(tenancy: flip first, isolation tests pass, then collapse local
App\\Entity\\Group onto Waaseyaa\\Groups\\Group).
docs/specs/bundle-scoped-storage.md (~30 lines added across 4 sites):
- Naming section cross-references the K1 single helper + structural
guard at EntityTypeManager::addBundleFields().
- Lifecycle "Runtime notice on save-path mismatch" paragraph
strengthened: explicit K4 framing — once-per-(entity_type, bundle)
per process, memoized on bundle-subtable cache, no throw, same
pattern on load path.
- Drift diagnostic section gains K5 (dialect portability via
AbstractSchemaManager::listTableNames + sqlite_master fast-path)
and K6 (HealthChecker layer placement codified via #824 surface C
KERNEL_EXEMPT_FILES, citing K6(c)).
- References section adds links to entity-system.md tenancy section
and to the mission spec.
Path X executes in sub-task 2 (issue closures) — not part of this
commit.
Drafts under .kittify/missions/1257-entity-storage-hardening/wp02-drafts/
are committed alongside as an audit trail of the review process for
sub-tasks 2/3/5.
Gates: bin/check-package-layers OK; bin/check-composer-policy OK.
Markdown-only diff; phpstan/cs-check/tests not materially affected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(#1257): WP02 sub-task 5 — HasCommunityInterface deprecation note
Adds a `### Deprecated` entry plus a full operator-facing migration
recipe to packages/groups/CHANGELOG.md "## Unreleased" per mission
#1257 WP02 sub-task 5 (contract C1, implementation lands in WP10).
The recipe walks consumers through the four-step migration (declare
tenancy: on EntityType registration → verify isolation tests →
remove HasCommunityInterface from the class → confirm deprecation
log stops firing), and includes a Minoo-specific subsection prescribing
the transition order for adopters running a local App\\Entity\\Group
alongside the composer dep on waaseyaa/groups.
Removal scheduled for the next minor release. The deprecation cycle
in this release is non-breaking — HasCommunityInterface continues to
function, with one-time LoggerInterface::warning() per (entity-type
id, process) on first wiring.
Cross-references docs/specs/entity-system.md §Community Scoping
(blessed in 3054b498e) and the mission spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#1257): mark WP02 done — spec execution + doc update complete
Updates mission tasks.md to reflect WP02 acceptance. All five sub-tasks
landed:
Sub-task 1 — verified #824 S1 exemption-surface in bin/check-package-layers
(commit a07d80f4f); HealthChecker named per K6(c).
Sub-task 2 — closed #1298 #1299 #1300 #1301 #1304 #1308 #1313 with
cross-link comments. Anchor #1257 remains open per Path X.
Sub-task 3 — blessed K1-K7 + C1 in entity-system.md + bundle-scoped-storage.md
(commit 3054b498e).
Sub-task 4 — D2 verified: collision exception already names both registrants
(WP07 part B becomes no-op). D3 verified: Group is final; Minoo
adopter partial (parallel local Group in transition — recipe
addresses the order).
Sub-task 5 — HasCommunityInterface deprecation entry + migration recipe in
packages/groups/CHANGELOG.md (commit 52bbdcc99).
Effect on mission: WP03–WP11 are now unblocked per the dependency chain
in tasks.md (line 23). Linear ordering still applies between WP03→WP04→WP05
because they all touch SqlEntityQuery::resolveField(); WP06/WP07/WP08/WP10
can run in parallel; WP09→WP08 if K6(a) had been chosen but K6(c) shipped
in WP02 sub-task 1, so WP08 is now verification-only post-Surface-C; WP11
is the charter lock and depends on all preceding WPs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#824): HttpServiceResolverInterface — SSR controller-method DI seam (WP02 surface F)
Mirrors the typed-resolver pattern from surface A (KernelServicesInterface).
Replaces the legacy `\Closure(string): ?object` passed to SSR with a typed
contract; default impl walks providers + narrows to a kernel-services
fallback. Existing closure-based kernel tests are redundant with the new
HttpKernelServiceResolverTest contract suite — deleted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(#824): mark WP02 done (surfaces A–F landed)
WP02 acceptance criteria satisfied:
- bin/check-package-layers passes against live graph (surfaces D, E)
- KernelServicesInterface ratified in infrastructure.md (surface A)
- No kernelResolver Closure refs in packages/foundation/src/ (surface A)
- extension-compatibility-matrix.md matches composer.json graph (surface E)
Surface F (HttpServiceResolverInterface) extended the typed-resolver
pattern to SSR for consistency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#824): ServiceProviderInterface completeness + signature tightening (WP03 surface A)
WP03 surface A: lock down the public ServiceProvider contract.
Interface (packages/foundation/src/ServiceProvider/ServiceProviderInterface.php):
- Add 5 missing methods the kernel actually calls: getBindings(),
resolve(), setKernelContext(), setKernelServices(),
getEntityTypeRegistrations()
- Fix routes() arity: now requires (WaaseyaaRouter, EntityTypeManager)
- resolve() return tightened from mixed to object (throws on missing)
Abstract base (ServiceProvider):
- Remove optional defaults that violate the no-optional-params rule:
routes() second arg is required; httpDomainRouters() takes
HttpKernel directly; setKernelContext() requires manifestFormatters
- Replace mixed renderCacheBackend with ?CacheBackendInterface
- Tighten resolve(): object with explicit non-object guard
Overrides updated to match: api, admin-surface, debug, genealogy,
graphql, mcp, media, routing, ssr. Dead null-fallback paths removed.
Tests updated: 17 setKernelContext calls now pass empty formatters,
routes()/httpDomainRouters() call sites pass real EntityTypeManager /
HttpKernel. ApiServiceProviderTest's null-fallback test deleted (its
premise no longer exists).
Surface B (next): split HTTP-specific hooks (routes, middleware,
configureHttpKernel, etc.) into a capability interface, swap
EntityTypeManager → EntityTypeManagerInterface in the contract, retype
WaaseyaaRouter to a RouterInterface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#824): ServiceProvider contract test (WP03 surface B)
WP03 surface B: lock down the ServiceProvider extension contract so
ServiceProviderInterface, the abstract ServiceProvider base, and the
kernel call sites cannot drift apart silently.
New test (packages/foundation/tests/Contract/ServiceProviderContractTest.php)
asserts five invariants via reflection (DIR-008 contract-test reflection
exemption):
- abstract ServiceProvider implements every interface method;
- ServiceProviderInterface roster matches an explicit allowlist
(adding/removing a method is now a deliberate, visible change);
- every $provider->X() invocation under packages/foundation/src/Kernel
resolves to either an interface method, an ABSTRACT_BASE_ONLY
capability-split candidate, or a CAPABILITY_INTERFACES entry guarded
by instanceof at the call site;
- no interface method has optional parameters (DIR-003);
- every interface method declares a single concrete return type, no
union/intersection, no mixed (DIR-005).
phpunit.xml.dist: extend the Unit testsuite glob to include
packages/foundation/tests/Contract so the new test actually runs under
composer test and lefthook's pre-commit hook. Scoped to foundation only
to avoid sweeping in latent contract tests from other packages.
Verified: cs-check clean, phpstan max clean (1046 files, 0 errors),
package-layers OK, composer-policy OK, Unit suite 5831 tests / 12944
assertions OK.
Capability-split work (commands, middleware, configureHttpKernel,
registerRenderCacheListeners, httpDomainRouters,
graphqlMutationOverrides) remains parked on the abstract base by
design — see ServiceProviderInterface § "Capability split" — and is
encoded in ABSTRACT_BASE_ONLY so the test will fail loudly if any of
those hooks is removed without a split plan.
Issues #833 #838 #843 closed by interface lockstep guarantee.
* docs(#824): ServiceProvider hook enumeration (WP03 surface C)
WP03 surface C: bring the spec docs in step with the
ServiceProviderInterface contract that surfaces A and B locked down,
so framework documentation, the contract test, and the kernel call
sites all tell the same story.
infrastructure.md gains a new "ServiceProvider extension hooks"
subsection that enumerates all three tiers of provider hooks the
kernel invokes during bootstrap:
- Tier 1: the 10 ServiceProviderInterface methods (public contract).
- Tier 2: the 6 abstract-base capability-split candidates that the
kernel calls today and that WP03 surface D will lift into named
capability interfaces.
- Tier 3: the one capability interface already in use
(LanguagePathStripperInterface::stripLanguagePrefixForRouting),
guarded by instanceof at the call site.
Each row names the kernel call site that invokes the hook, so anyone
adding a method now has a single place to look for the contract.
The subsection also points to the contract test
(packages/foundation/tests/Contract/ServiceProviderContractTest.php)
as the lockstep gate that will fail if interface, abstract base,
allowlists, or kernel call sites drift apart.
plugin-extension-points.md previously duplicated (and contradicted)
the hook list — it still referenced the long-removed
setKernelResolver(\Closure) and the optional ?EntityTypeManager that
WP03 surface A made required. The duplicated section is collapsed
into a single pointer to infrastructure.md so there is exactly one
authoritative description of the hook surface.
Stale specs api-layer.md, debugging-dx.md, and mcp-endpoint.md flagged
by the drift detector belong to their own subsystem missions and are
intentionally out of scope for WP03 surface C.
WP03 acceptance criterion "infrastructure.md enumerates the full hook
list" satisfied.
* feat(#824): HasGraphqlMutationOverridesInterface capability split (WP03 surface D)
WP03 surface D: lift the first of six abstract-base capability hooks
into a named capability interface, following the
LanguagePathStripperInterface precedent.
graphqlMutationOverrides() is the lowest-risk starting point — zero
provider overrides exist in the monorepo, the only call site lives in
packages/graphql/src/GraphQlServiceProvider, and PHPStan max stays
clean. Surface D establishes the pattern that surfaces E–I will reuse
for commands, middleware, httpDomainRouters,
registerRenderCacheListeners, and configureHttpKernel.
Changes:
- New: packages/foundation/src/ServiceProvider/Capability/
HasGraphqlMutationOverridesInterface.php — capability marker
declaring the contract method with full PHPStan-typed signature.
- packages/foundation/src/ServiceProvider/ServiceProvider.php — drop
the now-unused graphqlMutationOverrides() no-op default. Removing
it is safe because no concrete provider overrode it.
- packages/graphql/src/GraphQlServiceProvider.php — guard the loop
with `if (!$provider instanceof HasGraphqlMutationOverridesInterface)
continue;` before invoking the hook. Future GraphQL contributors
declare `implements HasGraphqlMutationOverridesInterface`.
- packages/foundation/tests/Contract/ServiceProviderContractTest.php —
move 'graphqlMutationOverrides' from ABSTRACT_BASE_ONLY into
CAPABILITY_INTERFACES (mapped to the new interface). The
CAPABILITY_INTERFACES docblock now notes that some entries gate
cross-package call sites (GraphQL bootstrap), not just foundation
kernel call sites.
- docs/specs/infrastructure.md — move the row from Tier 2 to Tier 3
in the ServiceProvider extension hooks table; add a spec-reviewed
marker recording the split.
Verified: cs-check clean, PHPStan max clean (0 errors / 1047 files),
package-layers OK, composer-policy OK, Unit suite 5831 tests / 12944
assertions OK, ServiceProviderContractTest 5/5 passing.
Surface D completes; surfaces E–I remain to lift the other five hooks
into capability interfaces. The lockstep guarantee tightens with each
landing.
* feat(#824): HasCommandsInterface capability split (WP03 surface E)
WP03 surface E: lift commands() from the abstract ServiceProvider base
into HasCommandsInterface. Same pattern as surface D, but this hook is
called directly from the foundation kernel (ConsoleKernel::handle), so
the contract test's everyKernelCallSiteResolvesToInterfaceOrAbstractBase
scan actually exercises the new CAPABILITY_INTERFACES entry.
Changes:
- New: packages/foundation/src/ServiceProvider/Capability/
HasCommandsInterface.php — capability marker declaring the existing
signature (EntityTypeManager, DatabaseInterface,
EventDispatcherInterface).
- packages/foundation/src/ServiceProvider/ServiceProvider.php — drop
the unused commands() no-op default. Tier 2 down to 4 candidates.
- packages/foundation/src/Kernel/ConsoleKernel.php — guard the loop
with `if (!$provider instanceof HasCommandsInterface) continue;`
before invoking. Adds the new import.
- packages/northcloud/src/Provider/NorthCloudServiceProvider.php —
declare `implements HasCommandsInterface` (the only concrete
overrider in the monorepo). The override signature was already
identical to the new interface, so no body changes needed.
- packages/foundation/tests/Contract/ServiceProviderContractTest.php —
move 'commands' from ABSTRACT_BASE_ONLY into CAPABILITY_INTERFACES.
This time the kernel-call-site scan finds $provider->commands(...)
in ConsoleKernel.php and routes it through the CAPABILITY_INTERFACES
branch, so the contract test now actively guards the new wiring.
- docs/specs/infrastructure.md — move the row from Tier 2 to Tier 3,
add a spec-reviewed marker recording the split.
Verified: cs-check clean, PHPStan max clean (0 errors / 1048 files),
package-layers OK, composer-policy OK, Unit suite 5831 tests / 12944
assertions OK, ServiceProviderContractTest 5/5 passing.
Surfaces F–I remain to lift the remaining four hooks (middleware,
httpDomainRouters, registerRenderCacheListeners, configureHttpKernel)
into capability interfaces.
* feat(#824): HasRenderCacheListenersInterface capability split (WP03 surface F)
WP03 surface F: lift registerRenderCacheListeners() from the abstract
ServiceProvider base into HasRenderCacheListenersInterface. Same
recipe as surfaces D and E.
Changes:
- New: packages/foundation/src/ServiceProvider/Capability/
HasRenderCacheListenersInterface.php — capability marker preserving
the existing nullable second param so HttpKernel can keep passing
null when no render cache is configured.
- packages/foundation/src/ServiceProvider/ServiceProvider.php — drop
the registerRenderCacheListeners() no-op default. Two imports
(Symfony\Contracts\EventDispatcher\EventDispatcherInterface and
Waaseyaa\Cache\CacheBackendInterface) became unused once the method
left and are removed alongside it. Tier 2 down to 3 candidates.
- packages/foundation/src/Kernel/HttpKernel.php — guard the loop in
finalizeBoot with `if (!$provider instanceof
HasRenderCacheListenersInterface) continue;` before invoking. New
import inserted alphabetically inside the Foundation group.
- packages/ssr/src/SsrServiceProvider.php — declare `implements
HasRenderCacheListenersInterface, LanguagePathStripperInterface`
(alphabetical). Existing override signature already matches the new
interface, no body changes.
- packages/foundation/tests/Contract/ServiceProviderContractTest.php
— move 'registerRenderCacheListeners' from ABSTRACT_BASE_ONLY into
CAPABILITY_INTERFACES mapped to the new interface. Tier 3 up to 4.
- docs/specs/infrastructure.md — move the row from Tier 2 to Tier 3
in the ServiceProvider extension hooks table; add a spec-reviewed
marker.
Verified: cs-check clean, PHPStan max clean (0 errors / 1049 files),
package-layers OK, composer-policy OK, Unit suite 5831 tests / 12944
assertions OK, ServiceProviderContractTest 5/5 passing.
Surfaces G–I remain to lift configureHttpKernel, middleware, and
httpDomainRouters into capability interfaces.
* feat(#824): ConfiguresHttpKernelInterface capability split (WP03 surface G)
WP03 surface G: lift configureHttpKernel() from the abstract
ServiceProvider base into a verb-led capability marker. Same recipe
as surfaces D/E/F. The verb-led name (Configures*, not Has*) reflects
that this hook mutates the kernel rather than contributing a list of
values — naming convention to follow as new state-mutating hooks
emerge.
Changes:
- New: packages/foundation/src/ServiceProvider/Capability/
ConfiguresHttpKernelInterface.php — capability marker carrying the
existing single-arg signature (HttpKernel $kernel).
- packages/foundation/src/ServiceProvider/ServiceProvider.php — drop
the configureHttpKernel() no-op default. Tier 2 down to 2
candidates (middleware, httpDomainRouters).
- packages/foundation/src/Kernel/HttpKernel.php — guard the
finalizeBoot loop with `if (!$provider instanceof
ConfiguresHttpKernelInterface) continue;` before invoking. New
import sorted alphabetically inside the Foundation group above
HasRenderCacheListenersInterface.
- packages/genealogy/src/GenealogyServiceProvider.php — declare
`implements ConfiguresHttpKernelInterface`. Existing override
signature already matches; no body changes. Genealogy uses this
hook to prime GenealogyBootstrap's static service references for
legacy callers without constructor DI.
- packages/ssr/src/SsrServiceProvider.php — extend the implements
list to include ConfiguresHttpKernelInterface (alphabetical, three
capability interfaces total now).
- packages/foundation/tests/Contract/ServiceProviderContractTest.php
— move 'configureHttpKernel' from ABSTRACT_BASE_ONLY into
CAPABILITY_INTERFACES mapped to the new interface. Tier 3 up to 5.
- docs/specs/infrastructure.md — move the row from Tier 2 to Tier 3
in the ServiceProvider extension hooks table; add a spec-reviewed
marker.
Verified: cs-check clean, PHPStan max clean (0 errors / 1050 files),
package-layers OK, composer-policy OK, Unit suite 5831 tests / 12944
assertions OK, ServiceProviderContractTest 5/5 passing.
Surfaces H and I remain to lift the last two hooks (middleware,
httpDomainRouters) into capability interfaces.
* feat(#824): HasMiddlewareInterface capability split (WP03 surface H)
WP03 surface H: lift middleware() from the abstract ServiceProvider
base into HasMiddlewareInterface. Same recipe as surfaces D/E/F/G.
Three concrete overriders updated.
Changes:
- New: packages/foundation/src/ServiceProvider/Capability/
HasMiddlewareInterface.php — capability marker carrying the
EntityTypeManager-typed signature and HttpMiddlewareInterface
return type.
- packages/foundation/src/ServiceProvider/ServiceProvider.php — drop
the middleware() no-op default. Tier 2 down to 1 candidate
(httpDomainRouters).
- packages/foundation/src/Kernel/HttpKernel.php — guard the
buildMiddlewarePipeline loop with `if (!$provider instanceof
HasMiddlewareInterface) continue;` before invoking. New import
inserted alphabetically inside the Foundation group between
ConfiguresHttpKernelInterface and HasRenderCacheListenersInterface.
- packages/auth/src/AuthServiceProvider.php — declare `implements
HasMiddlewareInterface`.
- packages/debug/src/DebugServiceProvider.php — declare `implements
HasMiddlewareInterface`.
- packages/inertia/src/InertiaServiceProvider.php — declare
`implements HasMiddlewareInterface`. All three override signatures
already match the new interface, no body changes.
- packages/foundation/tests/Contract/ServiceProviderContractTest.php
— move 'middleware' from ABSTRACT_BASE_ONLY into
CAPABILITY_INTERFACES mapped to the new interface. Tier 3 up to 6.
- docs/specs/infrastructure.md — move the row from Tier 2 to Tier 3
in the ServiceProvider extension hooks table; add a spec-reviewed
marker.
Verified: cs-check clean, PHPStan max clean (0 errors / 1051 files),
package-layers OK, composer-policy OK, Unit suite 5831 tests / 12944
assertions OK, ServiceProviderContractTest 5/5 passing.
Surface I remains to lift the last hook (httpDomainRouters) — the
largest concrete consumer footprint (4 overriders: api, graphql,
media, ssr) — into a capability interface, after which the abstract
ServiceProvider base contains zero capability hooks and the lockstep
guarantee is complete.
* feat(#824): HasHttpDomainRoutersInterface capability split (WP03 surface I)
WP03 surface I — the FINAL surface. Lifts the last kernel-invoked
hook out of the abstract ServiceProvider base. After this commit the
abstract base contains zero capability hooks: every method on it
either belongs to ServiceProviderInterface (the public contract) or
is a helper for subclasses. Kernel call sites all dispatch through
either the typed interface or an instanceof-guarded capability
interface — drift physically cannot land silently.
Changes:
- New: packages/foundation/src/ServiceProvider/Capability/
HasHttpDomainRoutersInterface.php — capability marker carrying the
HttpKernel-typed signature and DomainRouterInterface return type.
- packages/foundation/src/ServiceProvider/ServiceProvider.php — drop
the httpDomainRouters() no-op default. The DomainRouterInterface
import becomes orphan and is removed alongside it. Tier 2 down to
zero candidates by design.
- packages/foundation/src/Kernel/HttpKernel.php — guard the
buildDomainRouterChain loop with `if (!$provider instanceof
HasHttpDomainRoutersInterface) continue;` before invoking. New
import sorted alphabetically inside the Foundation Capability
group between ConfiguresHttpKernelInterface and
HasMiddlewareInterface.
- packages/api/src/ApiServiceProvider.php,
packages/graphql/src/GraphQlServiceProvider.php,
packages/media/src/MediaServiceProvider.php,
packages/ssr/src/SsrServiceProvider.php — declare `implements
HasHttpDomainRoutersInterface`. Existing override signatures
already match the new interface, no body changes. SsrServiceProvider
now implements four capability interfaces total
(ConfiguresHttpKernelInterface, HasHttpDomainRoutersInterface,
HasRenderCacheListenersInterface, LanguagePathStripperInterface).
- packages/foundation/tests/Contract/ServiceProviderContractTest.php
— move 'httpDomainRouters' from ABSTRACT_BASE_ONLY into
CAPABILITY_INTERFACES; ABSTRACT_BASE_ONLY is now empty by design.
The docblock makes the empty invariant explicit: new kernel-invoked
hooks must enter as capability interfaces, never as no-op defaults
on the abstract base. Tier 3 up to 7.
- docs/specs/infrastructure.md — Tier 2 section updated to record
that the list is empty by design after surfaces D–I; Tier 3 table
gains the final row; spec-reviewed marker records mission close.
Verified: cs-check clean, PHPStan max clean (0 errors / 1052 files),
package-layers OK, composer-policy OK, Unit suite 5831 tests / 12944
assertions OK, ServiceProviderContractTest 5/5 passing.
WP03 surfaces A–I complete:
A — ServiceProviderInterface completeness + signature tightening
B — Contract test (interface ↔ base ↔ kernel lockstep)
C — Spec doc enumeration (infrastructure.md hook table)
D — HasGraphqlMutationOverridesInterface (0 overriders)
E — HasCommandsInterface (1 overrider: northcloud)
F — HasRenderCacheListenersInterface (1 overrider: ssr)
G — ConfiguresHttpKernelInterface (2 overriders: genealogy, ssr)
H — HasMiddlewareInterface (3 overriders: auth, debug, inertia)
I — HasHttpDomainRoutersInterface (4 overriders: api, graphql,
media, ssr)
Mission #824 WP03 — service-provider-extension-contract — done.
Member issues #833 #838 #843 closed structurally by the lockstep
guarantee. Ready for WP04 (entity-type-manager-public-surface) per
the WP02 → WP03 → WP04 → WP05 → WP06 strict sequencing rule.
* docs(#824): EntityTypeManager reserved-namespace contract (WP04 surface A)
WP04 surface A: document the asymmetric registration contract that
EntityTypeManager already enforces but the spec didn't describe.
The spec previously listed registerEntityType() and
registerCoreEntityType() as separate signatures without explaining
how they differ. The actual contract is that the "core." id prefix
is reserved: registerEntityType() rejects "core.*" with a
[NAMESPACE_RESERVED] DomainException naming the offending id and
hinting at the remediation (use a custom prefix), while
registerCoreEntityType() bypasses the guard for kernel boot code
and core service providers. Beyond the namespace check both methods
share persistDefinition(), so registrant provenance, duplicate
detection, and shadow-collision behaviour apply equally.
Adds a "Reserved-namespace contract" subsection to docs/specs/
entity-system.md inside the existing EntityTypeManagerInterface
block, with a method-vs-id-vs-caller table and a note that the
"only kernel + core providers may call registerCoreEntityType"
expectation is convention rather than runtime enforcement (DIR-006
layer discipline + code review own that). Adds a spec-reviewed
marker so drift-detector will see entity-system.md as fresh against
the next round of EntityTypeManager source touches.
WP04 acceptance criterion "entity-system.md lists the full mutation
surface" satisfied for the registration methods. Closes #835.
Surfaces B (RevisionableStorageInterface spec ↔ source contract test)
and C (admin-surface interface-only typing) remain.
* feat(#824): RevisionableStorageInterface spec ↔ source contract test (WP03 surface B equivalent for WP04)
WP04 surface B: end the long-standing drift between
RevisionableStorageInterface in PHP and its description in
docs/specs/entity-system.md, then make sure that drift can never
silently land again.
The spec block had been wrong on every line: loadRevision and
deleteRevision were missing the entityId argument; loadMultipleRevisions
took the wrong second-arg shape; getLatestRevisionId was published
with an int|string|null return type when the source returns ?int;
and getRevisionIds was missing entirely. None of that was caught
because no test linked the two artifacts.
Changes:
- docs/specs/entity-system.md — rewrite the
### RevisionableStorageInterface code block to match the source
one-to-one. Include all five revision methods with the actual
signatures, the array-shape PHPDoc, and a brief note on why
entityId is the first arg on every method (revision IDs are
unique only within an entity). Cross-reference the new contract
test as the lockstep gate. Add a spec-reviewed marker.
- New: packages/entity/tests/Contract/
RevisionableStorageInterfaceContractTest.php — three reflection-
only tests:
* interfaceMethodRosterMatchesDeclaration — pin the set of
methods the interface declares directly (parent
EntityStorageInterface methods are excluded by
getDeclaringClass filter).
* eachMethodSignatureMatchesExpectedShape — for every roster
entry, assert parameter count, parameter names, parameter
types (handles ReflectionUnionType for int|string), parameter
nullability, return type, and return nullability. Catches
drift down to the parameter-name level.
* specSectionDocumentsEveryRosterMethod — read
docs/specs/entity-system.md, extract the
### RevisionableStorageInterface section up to the next
"### " heading, and assert every roster method name appears
as `public function NAME(`. Closes the third leg of the
lockstep triangle (interface ↔ roster ↔ spec).
- phpunit.xml.dist — extend the Unit testsuite glob to include
packages/entity/tests/Contract so the new test runs under
composer test and lefthook. Mirrors the WP03 surface B precedent
for packages/foundation/tests/Contract; scoped to entity only.
Verified: cs-check clean, PHPStan max clean (0 errors / 1053 files),
package-layers OK, composer-policy OK, Unit suite 5834 tests / 12991
assertions OK (3 new tests, 47 new assertions),
RevisionableStorageInterfaceContractTest 3/3 passing.
WP04 acceptance criterion "RevisionableStorageInterface in spec
matches source one-to-one (verified by a doc-test or contract
test)" satisfied. Closes #837.
Surface C (admin-surface EntityTypeManagerInterface-only typing,
closes #836) is the last WP04 surface.
* feat(#824): admin-surface EntityTypeManagerInterface-only typing (WP04 surface C)
WP04 surface C: drop every concrete EntityTypeManager binding from
packages/admin* and accept only EntityTypeManagerInterface. The
admin-surface host has been a long-standing edge case where typing
discipline (DIR-002 framework discipline, DIR-003 architecture
discipline) was relaxed for convenience; this surface closes that
gap and makes the relaxation impossible to reintroduce silently
because the acceptance gate is a single grep.
Changes:
- packages/admin-surface/src/AdminSurfaceServiceProvider.php — drop
the concrete EntityTypeManager import, add the interface import,
retype routes() second parameter as EntityTypeManagerInterface.
PHP allows widening an override parameter to a supertype
(contravariance), so this is compatible with
ServiceProviderInterface::routes() which still types the
parameter as the concrete EntityTypeManager.
- packages/admin-surface/src/Host/GenericAdminSurfaceHost.php —
swap the constructor parameter type and the docblock from
EntityTypeManager to EntityTypeManagerInterface. The host calls
only getDefinitions(), hasDefinition(), and getStorage(), all of
which are on the interface, so no behavior changes.
- packages/admin-surface/tests/Unit/Host/GenericAdminSurfaceHostTest.php
— swap the import and 19 createMock(EntityTypeManager::class)
call sites plus two anonymous-class constructor parameters to use
the interface. PHPUnit can mock interfaces directly, so the
switch is mechanical; no behavior changes.
- docs/specs/admin-spa.md — add a "Host extension typing"
paragraph in the Session UI customization section pinning the
contract: hosts and AdminSurfaceServiceProvider::routes() accept
EntityTypeManagerInterface only, the acceptance gate is the
documented grep, subclasses must not narrow the parameter. New
spec-reviewed marker.
Acceptance verified: `grep -rn 'EntityTypeManager[^I]' packages/admin*`
returns no matches.
Verified: cs-check clean, PHPStan max clean (0 errors / 1053 files),
package-layers OK, composer-policy OK, Unit suite 5834 tests / 12991
assertions OK, admin-surface package suite 100/318 OK.
Closes #836. WP04 (entity-type-manager-public-surface) is now done:
A — Reserved-namespace contract doc (closes #835)
B — RevisionableStorageInterface spec ↔ source contract test (closes #837)
C — admin-surface EntityTypeManagerInterface-only typing (closes #836)
Mission progress on #824: WP01 + WP02 (6 surfaces) + WP03
(9 surfaces) + WP04 (3 surfaces) = 18 surfaces done across 4 work
packages. WP05 (access-checker-placement-and-paired-nullable, S4,
members #832 #834 #844) is next per the WP02→WP03→WP04→WP05→WP06
strict sequencing rule.
* docs(#824): AccessChecker canonical placement (WP05 surface A)
WP05 surface A: end the long-standing contradiction between source
and spec on where AccessChecker lives. The class file is at
packages/access/src/AccessChecker.php with namespace Waaseyaa\Access
— a layer-1 package — but both access-control.md and api-layer.md
documented it as belonging to packages/routing. Anyone going by the
spec to reach for the import path or to reason about layer
discipline got the wrong picture.
Changes:
- docs/specs/access-control.md
* Package table: AccessChecker added to the access row (with the
"route-level access" note); removed from the routing row, with a
one-line note pointing to WP05 surface A so the move is
discoverable.
* "## Route Access Control" subsection: File/Namespace header
corrected to packages/access/src/AccessChecker.php and
Waaseyaa\Access; added a sentence explaining why the class
belongs in access (route-level access checking is the
routing-time consumer of gates/policies/account context, and
the dependency arrow points routing → access, never the
reverse).
* Filesystem dir-tree visualization: AccessChecker.php moved
under packages/access/src/ (alongside RedirectValidator.php and
ErrorPageRendererInterface.php at the package root); the orphan
"packages/routing/src/" stub that contained only
AccessChecker.php is removed.
* New spec-reviewed marker.
- docs/specs/api-layer.md
* Package surface table row for AccessChecker: namespace fixed
to Waaseyaa\Access; description annotated with the package move
and the layer rationale.
* AccessChecker code block comment: file path corrected and the
fully-qualified class name appended so an agent that reads the
code block sees the right import.
* Routing-package dir-tree visualization: AccessChecker.php line
removed (it was never under packages/routing/src/ in the
actual source tree).
* New spec-reviewed marker.
No code changes, no contract test in this surface — surfaces B
(ResourceSerializer paired-nullable enforcement) and C
(SchemaPresenter same) carry the runtime work and the negative-path
tests. Per the WP05 acceptance gate, access-control.md and
api-layer.md now agree on AccessChecker placement.
Closes #832.
* feat(#824): ResourceSerializer paired-nullable access-context precondition (WP05 surface B)
WP05 surface B: enforce the paired-nullable invariant on
ResourceSerializer::serialize() and serializeCollection() that
WP05 surface A documented but the runtime never checked. Both
methods accept (?EntityAccessHandler $handler, ?AccountInterface
$account); the contract is "both null or both non-null". A mixed
state silently degraded the JSON:API output before — the field
filter was skipped without diagnosis. From now on, mixed state
throws a typed exception.
Changes:
- New: packages/access/src/Exception/PartialAccessContextException.php
— final LogicException with a forSerializer(string $callerMethod)
named-constructor that produces the [PARTIAL_ACCESS_CONTEXT]
diagnostic prefix and a remediation hint. Lives in the access
package because the access-context concept is owned there;
surface C will reuse it from SchemaPresenter.
- packages/api/src/ResourceSerializer.php — single-line guard at
the top of serialize() and serializeCollection():
if (($accessHandler === null) !== ($account === null)) {
throw PartialAccessContextException::forSerializer(__METHOD__);
}
XOR-style check with one comparison, fires only on mixed state,
zero overhead in the both-null and both-non-null fast paths.
serializeCollection() guards independently of serialize() for
fail-fast at the entry point with a single error message instead
of N identical exceptions.
- New: packages/api/tests/Unit/ResourceSerializerPartialContextTest.php
— 6 focused tests / 8 assertions covering all four context
states: both-null (returns JsonApiResource), both-non-null
(returns JsonApiResource), handler-without-account (throws),
account-without-handler (throws); the latter two also covered on
serializeCollection(). Throw cases assert the
[PARTIAL_ACCESS_CONTEXT] diagnostic prefix to lock the contract
message format.
Verified: cs-check clean, PHPStan max clean (0 errors / 1054 files),
package-layers OK, composer-policy OK, Unit suite 5840 tests / 12999
assertions OK (6 new tests, 8 new assertions),
ResourceSerializerPartialContextTest 6/6 passing.
Breaking change scope (per S4 decomposition): callers that were
passing partial access context will now get a typed exception
instead of silently degraded output. This is the intended outcome —
silent degradation was the bug.
Closes #834. Surface C (SchemaPresenter same enforcement, closes
#844) is the last WP05 surface and will reuse
PartialAccessContextException::forSerializer.
* feat(#824): SchemaPresenter paired-nullable access-context precondition (WP05 surface C)
WP05 surface C: same recipe as surface B applied to the other
half of the access-context surface. SchemaPresenter::present()
accepts (?EntityAccessHandler, ?AccountInterface) for the same
purpose as ResourceSerializer::serialize() — drive read-time field
filtering — and had the same drift between the documented
paired-nullable invariant and the runtime, which never enforced
it. Mixed nullability silently degraded the JSON Schema output by
skipping the field-access trim.
Changes:
- packages/api/src/Schema/SchemaPresenter.php — import
PartialAccessContextException; single-line XOR-style guard at
the top of present():
if (($accessHandler === null) !== ($account === null)) {
throw PartialAccessContextException::forSerializer(__METHOD__);
}
Reuses the named-constructor introduced in surface B; no new
exception class, no new diagnostic prefix to remember. The body
of present() is otherwise unchanged.
- New: packages/api/tests/Unit/Schema/
SchemaPresenterPartialContextTest.php — 5 focused tests / 7
assertions: both-null returns a populated schema, both-non-null
returns a populated schema, handler-without-account throws,
account-without-handler throws, plus a sanity test that an
EntityTypeInterface mock is never touched before the precondition
fires (proves the guard is the very first statement in the
method body).
Verified: cs-check clean, PHPStan max clean (0 errors / 1055 files),
package-layers OK, composer-policy OK, Unit suite 5845 tests / 13006
assertions OK (5 new tests, 7 new assertions),
SchemaPresenterPartialContextTest 5/5 passing.
Closes #844. WP05 (access-checker-placement-and-paired-nullable) is
now done:
A — AccessChecker canonical placement docs (closes #832)
B — ResourceSerializer paired-nullable precondition (closes #834)
C — SchemaPresenter paired-nullable precondition (closes #844)
Mission #824 progress: WP01 + WP02 (6 surfaces) + WP03 (9 surfaces)
+ WP04 (3 surfaces) + WP05 (3 surfaces) = 21 surfaces, 5 work
packages, 17 issues closed. WP06 (jsonapi-route-and-discovery-
surface, S5, single member #841) is next per the strict
WP02→WP03→WP04→WP05→WP06 sequencing rule, and is also the smallest
remaining WP.
* feat(#824): JsonApiRouteProvider api.discovery surface (WP06 surface A)
- docs/specs/api-layer.md: JsonApiRouteProvider route table now lists
the public api.discovery route alongside the five per-entity-type
CRUD routes; access column documents _public/_authenticated semantics
- docs/specs/api-layer.md: new ApiDiscoveryController section documents
the response contract — meta {api: 'waaseyaa', version: '1.0'} and
links {self, <entity_type_id>: {href, meta.type}} — and ties the
contract back to JsonApiRouteProvider and DiscoveryRouter dispatch
- docs/specs/api-layer.md: add 2026-05-01 spec-reviewed marker
- tests/Integration/Phase7/ApiDiscoveryIntegrationTest.php: end-to-end
exercise of the documented surface — JsonApiRouteProvider registers
api.discovery as public GET /api with the documented controller
default; WaaseyaaRouter matches /api to api.discovery; discover()
returns the contract envelope; discovery hrefs resolve back to the
matching api.{type}.index routes; empty manager collapses to self;
custom basePath flows through to all hrefs
Verified:
- composer cs-check: pass
- composer phpstan: 1053/1053 files, 0 errors
- bin/check-composer-policy: pass
- bin/check-package-layers: 3 pre-existing failures on
foundation/src/ServiceProvider/Capability/Has*Interface (out of
WP06 scope, present on main HEAD before this surface)
- Unit suite: 5845 tests / 13006 assertions (baseline match)
- Integration suite: +7 tests / +28 assertions in
ApiDiscoveryIntegrationTest (all green); 3 pre-existing failures
(McpEndpointSmokeTest, OidcTokenIntegrationTest,
PublicSurfaceVerificationTest) unchanged from baseline
Closes #841
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#824): admin-surface AdminSurfaceAccount.emailVerified contract field (WP07 surface A)
- packages/admin-surface/contract/types.ts: AdminSurfaceAccount gains
optional `emailVerified?: boolean`, with TSDoc explaining provenance
(PHP host AdminSurfaceSessionData::toArray() emits it; SPA runtime
reads it in auth.global and VerificationBanner) and the optional
semantics (hosts without verification may omit; SPA treats absent
as unverified for gating)
- docs/specs/admin-spa.md: align spec language with the contract —
ensureVerifiedEmail middleware and VerificationBanner component
sections now reference camelCase `emailVerified` and
`requireVerifiedEmail`, eliminating the third snake_case naming
variant flagged in the audit
- docs/specs/admin-spa.md: add 2026-05-01 spec-reviewed marker
Verified:
- composer cs-check: pass
- composer phpstan: 0 errors
- bin/check-composer-policy: pass
- bin/check-package-layers: 3 pre-existing failures unchanged
(foundation/src/ServiceProvider/Capability/Has*Interface)
- AdminSurface-filtered Unit suite: 100 tests / 318 assertions
Closes #839
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#824): admin-surface catalog description regression anchor (WP07 surface B)
- packages/admin-surface/tests/Unit/Catalog/CatalogBuilderTest.php:
add regression coverage for AdminSurfaceCatalogEntry.description —
asserts the PHP host emits `description` in the catalog payload when
set via the fluent builder, and that it is omitted entirely when
unset (matching the optional `description?: string` contract field
in packages/admin-surface/contract/types.ts)
- docs/specs/admin-spa.md: add 2026-05-01 spec-reviewed marker
acknowledging that #840 is anchored by the new test, so the contract
field cannot silently regress
The contract type and host emission already exist; this surface locks
them down with explicit verification, which the audit (#840) had
identified as a coverage gap on the admin-surface integration boundary.
Verified:
- composer cs-check: pass
- composer phpstan: 0 errors
- bin/check-composer-policy: pass
- bin/check-package-layers: 3 pre-existing failures unchanged
- CatalogBuilderTest: 10 tests / 35 assertions (was 8 / 30; +2 / +5)
Closes #840
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#824): admin-surface cross-boundary contract conformance test (WP07 surface C)
- tests/Integration/AdminSurface/AdminSurfaceContractConformanceTest.php:
new root-level integration test that asserts backend-emitted
admin-surface payloads conform structurally to the TypeScript
contract at packages/admin-surface/contract/types.ts. Drift on
either side breaks the test:
* gap (PHP omits required field) — every non-optional TS interface
key must appear in the PHP payload
* drift (PHP emits unknown field) — every PHP payload key must
have a corresponding TS interface field
Covers four interfaces: AdminSurfaceSession, AdminSurfaceAccount,
AdminSurfaceTenant, AdminSurfaceCatalogEntry, AdminSurfaceCapabilities.
The contract parser is a small regex over the simple interface shape
in types.ts; a contractParserExtractsExpectedRoster sanity test
guards the parser itsel…