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.
m365-governance migration-verify baseline.json verification.json `
--kind tenant-to-tenant --performed-by "a migration tool" `
--out record.json --report report.mdWhat it prints
From a real run, anonymised:
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 0005d0cf8ed794b660ab3032cba5f6d62ffbe8bb4150e0a0949f8919aa03550bTwo 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
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:
| Refusal | Why |
|---|---|
| a baseline that is not earlier than the verification | it is not a baseline |
| the same read handed in twice | there is nothing to compare |
| two reads taken by identities with different access | every absent item is then a permission difference wearing the clothes of a loss |
a size-only comparison concluding that content matched | two 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.