feat: Worker portal Stage 3 — composition loader (TOML parse + role filter + override merge)

Description

Build the composition loader — the runtime component that resolves a (jurisdiction, role, user, surface) request into a rendered template tree by walking the 5-layer storage model top-down (user delta → role override → jurisdiction live → jurisdiction baseline TOML → system defaults).

This is the core of the composability runtime. Stage 3 cannot proceed past this issue.

Acceptance Criteria

  • crates/canopy-composition/ (or whatever the Stage 2 composability-runtime ADR designates) holds the loader
  • Composition loader API:
    • load(jurisdiction, role, user, surface) -> ComposedSurface async
    • ComposedSurface carries panel/section list + per-item config (display_name, icon, span, programs)
  • TOML parser handles jurisdiction baseline files at rulesets/{jurisdiction}/composition/{surface}.toml
  • Role filter applies role-allowed permissions to filter out panels/sections the role can't see
  • Override merge: per Stage 2 ADR's chosen merge semantics (RFC 7396 / RFC 6902 / full replace)
  • Plugin manifest validator: validates Plugin.toml per Stage 2 composability ADR schema (slug unique, semver, allowed_spans subset, required roles exist, programs known)
  • Unit tests cover: TOML baseline only; baseline + jurisdiction live; baseline + role + user; conflicting layers (top wins); invalid Plugin.toml rejection
  • cargo nextest run -p canopy-composition clean
  • CHANGELOG entry under === Added

Blocked by

  • Stage 2 composability-runtime ADR merged
  • Stage 3 DB migrations issue merged (for override-table reads)

Context & References

Labels

type::feature, priority::medium, program::infrastructure, service::web, workflow::needs-spec