Skip to content

v0.1.0-alpha.175

@jonesrussell jonesrussell tagged this 01 May 17:17
* 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…
Assets 2
Loading