ClassificationResolver
Where an authoritative classification comes from — and where it does NOT, which is the half that matters. ── WHAT THIS RESOLVER REFUSES TO DO ──────────────────────────────────────────────────────────── Turn a claim into a classification. There is no route: not behind a flag, not «provisionally», not «while the attestation is on its way». A claim and an attestation are different types precisely so that the conversion does not exist as a representable move — which is GOV-02: an invariant is governed by removing the transition that would violate it, not by writing that it must not happen. ── THE PRECEDENCE RULE, AND WHY IT IS NOT A PREFERENCE ───────────────────────────────────────── When a valid attestation covers this digest, it governs. When there is none, the result is NOT what the package said about itself: it is `unclassified`, which under GOV-05 weighs as the worst case in every dimension. The claim is kept and returned — useful for knowing what somebody promised — but it does not enter the control derivation. The difference only shows in the case that matters: a package declaring `mutation: none` for an operation that sends email. With precedence, that claim wins whenever no attestation exists and the broker consumes it without suspicion. Without precedence, an unattested package is treated as what it is — something we know nothing about — and the convenient declaration buys it nothing. ── AND THE LIMIT THIS RESOLVER CANNOT COVER ──────────────────────────────────────────────────── Even with a perfect attestation, a PHP package in the same process can open a socket or run a command without going through any declared operation. Measured 2026-08-05 across the real family: **13 of 34 packages can, and they are ours.** While that holds, a classification describes what an operation SAYS it does, not what its package CAN do (GOV-12). This resolver closes the metadata chain; it does not close the process one.
ClassificationResolver::__construct()
public function __construct(array $attestations = []):Parameters
| Name | Type | Description |
|---|---|---|
| $attestations | list<ControlAttestation> |
ClassificationResolver::resolve()
public function resolve(string $package, string $digest, ?Milpa\Plugin\Supply\PackageCapabilityClaim $claim, int $moment): arrayThe classification governing this artifact — or the absence of one, said with its reason. The reason travels because «never classified» and «its attestation expired» ask different things of whoever reads them, and a boolean would make them indistinguishable.
Parameters
| Name | Type | Description |
|---|---|---|
| $package | string | |
| $digest | string | |
| $claim | ?Milpa\Plugin\Supply\PackageCapabilityClaim | |
| $moment | int |
ClassificationResolver::mayActivate()
public function mayActivate(Milpa\Plugin\Supply\PackageState $state, string $package, string $digest, ?Milpa\Plugin\Supply\PackageCapabilityClaim $claim, int $moment): boolWhether this artifact may receive capabilities. Being classified is not enough and being attested is not either: the package state must allow it too. These are two decisions — what is this, and do we let it act — and different people make them. Collapsing them would make classifying the same as granting.
Parameters
| Name | Type | Description |
|---|---|---|
| $state | Milpa\Plugin\Supply\PackageState | |
| $package | string | |
| $digest | string | |
| $claim | ?Milpa\Plugin\Supply\PackageCapabilityClaim | |
| $moment | int |