30-second answer
The engine's read-only gate parses every PowerShell file with the language's own parser and walks the syntax tree looking for mutating verbs. Green means two things and only two: every file is parseable, and no command with a mutating verb appears anywhere in it. It does not mean the collector runs, and it does not mean the collector is safe by any definition wider than verb absence. The engine's architecture document says this itself, next to the gate it describes.
What this proves
- Analysability.
Parser::ParseFilesucceeds on every file, so nothing hides from the walk behind a syntax error. - Verb absence. No
Set-,Remove-,New-,Add-or other mutating verb is called, in any file, including helpers. Tests refuse regressions by name: no PowerShell file has a write path, and no PowerShell file returns a conclusion.
What it does not prove
- Execution. Parsing is not running. The first version of
Get-ClassificationFactspassed the gate and threw on its first line against a real tenant. The gate was green because the gate measures text. - Behaviour reached through variables or REST. A verb walk sees command names. A URL assembled at runtime and posted somewhere is invisible to it, which is why the collector also refuses such patterns by construction rather than by scanner.
- That the modules load. A nested
Import-Module -Forceonce unloadedEvidence.psm1from the caller's scope, soInitialize-Evidencewas not recognized on every mode against every tenant, while every source-text gate stayed green. The fix removed-Forcefrom nested imports; the guard now loads the modules in the collector's own order and asks the live session which helpers survived.
PowerShell
# The same question the gate asks, on one file
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
'.\modules\Classification.psm1', [ref]$null, [ref]$null)
$ast.FindAll({ $args[0] -is
[System.Management.Automation.Language.CommandAst] }, $true) |
ForEach-Object { $_.GetCommandName() } |
Where-Object { $_ -match '^(Set|New|Remove|Add|Update|Clear)-' }Example output
# (no output: no mutating verb is called anywhere in the module)Explanation
A static gate and a runtime guarantee answer different questions, and the collector treats them as different questions. The verb walk proves a property of the text that survives every refactor and costs nothing to run in CI. The property it cannot prove, that the code works when loaded, is covered separately: a test loads the modules exactly as the collector loads them and verifies the session still has the helpers. The gap between the two is not theoretical; it shipped once, as the module-unloading defect above, and the engine records it as the reason the second guard exists.
Production considerations
- If your compliance story says "the collector is read-only because CI checks it", write down what the check measures. A gate that is honest about its limits survives an audit; a gate described as more than it is becomes the finding.
- Keep the verb list in one place and walk ALL files, including test helpers. A mutating call in a helper is a mutating call.
Related governance rules
SPO-CLASS-001: evaluated from facts the classification module reads, the module whose first version proved the limit of this gate
References
- Architecture: what the gates do not prove (m365-governance-as-code)
- Collector orchestrator (m365-governance-as-code)
Found something wrong? Suggest a correction. The article source is not public; the engine it cites is.