fix(policies): reject material policy attachments that can never apply

MI
1h ago
+395-14

Closes #3774

A policy without material types (kind-less) applies only to the material named by the attachment selector. A contract that attached such a policy without selector.name was accepted, but the policy never ran and nothing reported it. For a gating policy, this looked like a passing check.

Changes:

  • Control plane: contract create, update and apply now reject a material policy attachment that can never apply:

    • a kind-less policy without selector.name, for example: policy "upstream-workflow-attested" applies to any material type and requires a material selector (selector.name)
    • a typed policy whose exact selector names a contract material of a type that the policy does not support.

    A selector that names a material not declared in the contract, or a selector that is not an exact match (such as PREFIX), is still accepted, because it can match materials added at attestation time.

  • Attestation init: the same check runs when attestation init --contract <file> sends the contract, because that path also creates or updates the contract. A pipeline whose contract attaches a policy that can never apply now fails at init with the same error.

  • Shared rule: the applicability rule of shouldApplyPolicy moves into pure functions (attachmentApplies, canMatchAnyMaterial). Policy evaluation and the new validation both use them, so the two can't drift.

Not in scope:

  • Policy groups: a kind-less policy in an unnamed group material does not silently do nothing. The group verifier applies it to every material. This PR leaves that behavior unchanged.
  • policy develop eval --material-name: not part of this PR.
  • Embedded policies in contracts: they go through the provider lookup with an empty name in findAndValidatePolicy, which fails. This is a separate, existing issue.

This PR was written with AI assistance (Claude Code).