30-second answer

migration-verify takes two reads of the same estate, one from before a move and one from after it, and produces a record of what arrived and what did not. It is never produced by whatever performed the move: a report by the producer of the work is the producer's account of its own work, and that is the condition this contract exists to remove.

PowerShell
m365-governance migration-verify baseline.json verification.json `
  --kind tenant-to-tenant --performed-by "a migration tool" `
  --out record.json --report report.md

What it prints

From a real run, anonymised:

Text
contoso-projects
  baseline      baseline-001 (2026-07-04T06:10:22Z)
  verification  verification-001 (2026-08-11T05:58:41Z)
  compared      presence, count, content, size, authorship
  not compared  versions: at least one read carries no versions for the items
                they share, though the source is not declared unable to
                provide it
  not compared  permissions: at least one read carries no permissions for the
                items they share, though the source is not declared unable to
                provide it
  fail          2
  digest        0005d0cf8ed794b660ab3032cba5f6d62ffbe8bb4150e0a0949f8919aa03550b

Two things in that output are the product. compared lists what was actually examined, so a reader never has to infer from silence that a dimension was fine. not compared carries a reason for each one, and the reason distinguishes a limit of the source from a thinner read than the source allows: only the second is worth fixing by reading again.

unknown is not a failure, and the exit code says so

Text
1 if a fail or an invalid-evidence was established; 0 otherwise.

An operator who could not read half the estate has a coverage problem, not a migration problem, and conflating them is what this whole contract refuses. An unknown never moves the exit code. A missing item does.

What it refuses to produce

The record is refused rather than written when the inputs cannot support it, because printing a warning and continuing would put the operator's mistake inside a document that then travels as evidence:

RefusalWhy
a baseline that is not earlier than the verificationit is not a baseline
the same read handed in twicethere is nothing to compare
two reads taken by identities with different accessevery absent item is then a permission difference wearing the clothes of a loss
a size-only comparison concluding that content matchedtwo files of equal size differ

The report

--report renders the record for people; .html and .md are chosen by the extension. It states its own limits in the document, and carries no percentage, deliberately:

There is no percentage in this report, deliberately. A single number would be read instead of the sections, and the sections are what answers the question.

A difference is established only where both reads could see the item. Where either could not, the result is recorded as not established and never as a loss.

What it does not read

Age. An item dated 1984 is semantically absurd and legitimately survives transport, so nothing here infers loss from how old a timestamp is. The product may show a difference; it will not invent one from a date.

Exit codes

0 no failure established. 1 a failure or invalid evidence was established, or the record was refused as incoherent. 2 a file was not a migration read.