MigrationAdvisor
Turns a frozen {@see ResolutionReport} into an actionable {@see MigrationPlan} — spec §6's separation of duties, honoured strictly: the resolver detects and explains, the **Advisor proposes**, a command executes only if the human accepts, and the Academy teaches the concept. The advisor is PURE: it never touches the filesystem, the clock, or the network, and it never writes — the same inputs always yield the same plan, byte for byte. Raw materials, all read-only: the report's `legacy[]` paths (with their `migrationHints[]` targets), `missing[]` and `conflicts[]` entries, and the learnable `errors[]` whose `fixes[0]` becomes each step's action and whose live bilingual `learn.academy` links become the plan's `academy` pairs — nothing is invented, every step and link already exists in the diagnosis. Two caller-supplied inputs complete the picture (the same pattern as the engine's caller-owned `evaluatedAt` clock): - `$driftErrors` — the exact {@see LearnableArchitectureError} OBJECTS {@see \Milpa\Resolver\Ingest\DriftDetector::toLearnableErrors()} returns (code `MILPA_MANIFEST_DRIFT`, context `{package, fields}`). The objects, not their serialized arrays: the advisor stays pure either way, but the host's inspect surface already holds them verbatim, so this keeps the caller a pass-through. Entries with any other code are ignored. - `$hostProfile` — the profile the caller resolved against. `allowedLegacyContracts` and `acceptedRisks` live ONLY in the profile: the report's frozen `metadata` carries just the `hostProfile` name@version label and the verbatim `hostMetadata`, and the engine is frozen — injecting the allowlist into the report is not an option. So the caller, who built the profile to resolve in the first place, passes it here too; when it is omitted the plan's `compatibility` line says honestly that no window can be stated. No deadline is ever invented. Grouping rule: detections are grouped per package NAME — the attributing label (`legacy[]`'s `providedBy`, `missing[]`'s `requiredBy`, a drift error's `context.package`; conflicts attach to the host, whose label the report's `metadata.hostProfile` carries) with its `hostProfile:` scheme prefix and its `@version` suffix stripped, so `legacy/command-host@0.0.1` and a drift on `legacy/command-host` land in the SAME package entry. A package with nothing actionable never appears; a report with nothing actionable yields the visible empty plan (summary `0/0`).
MigrationAdvisor::advise()
public function advise(Milpa\Resolver\Report\ResolutionReport $report, array $driftErrors = [], ?Milpa\Resolver\Manifest\HostProfile $hostProfile = null): Milpa\Resolver\Advisor\MigrationPlanDerive the migration plan for a report: group the actionable detections per package, plan the steps from each diagnosis's first fix (numbered 1..n, the last ALWAYS the re-inspect), name the recommended targets, state the honest compatibility line, and attach the live Academy links the errors already carry. See the class DocBlock for the two caller-supplied inputs.
Parameters
| Name | Type | Description |
|---|---|---|
| $report | Milpa\Resolver\Report\ResolutionReport | |
| $driftErrors | list<LearnableArchitectureError> | The drift diagnoses the caller built via DriftDetector::toLearnableErrors() |
| $hostProfile | (HostProfile | null) | The profile resolved against — the only source of allowedLegacyContracts and acceptedRisks (they are not in the report) |