fix(intune): apply framework review discipline to the merged Autopilot lane - #531
Conversation
ADR-001: non-assessable evidence cannot produce a terminal conclusion. reduce_profile iterated observations raw, so a capped or unparsed success record could set retrieved/applied and, with an observed ESP handoff, prove Completed while the matching failure branch was filtered. The identity evidence loop had the same bypass and inflated the phase to IdentityObserved from an unreadable record. Closing the class, not the instance: sections_of now gates report sections on their own declared context (covering identity, profile, handoff, and outcome section paths), and the conflict-mismatch loop, correlation-key extraction, linkage evidence attribution, and the time-overlap probe are gated the same way. time_basis stays deliberately unfiltered because there the unfiltered scan is the conservative direction; documented at is_assessable. Also per ADR-003: reduce_outcome now matches a Conflicting ESP linkage explicitly and returns ContradictoryEvidence. The multi-session match path records no AutopilotConflict, so the empty-conflicts gate above it let an ambiguous session identity fall through to Completed. Refs #362, PR #450. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two silent-outcome gaps in the findings rules: - push_unknown_schema gated its capture branch on windows_build alone, so a capture declaring only an unvalidated autopilotSchemaVersion reduced to UnknownSchema with every terminal rule suppressed and no finding saying why. The gate now covers both declared values and the summary names whichever failed validation. - A Conflicting ESP linkage reached through distinct keys matching distinct sessions records no AutopilotConflict, so no rule explained it. push_esp_link_conflicting covers exactly that path; the single-key-many-sessions path stays with push_contradictory_evidence. ADR-001 (withheld semantics must still be explained) and ADR-003 (conservative representation of unresolved contradictions). Refs #362, PR #450. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mezone windows_zone_re accepted any alphabetic run of three or more characters, so placeholders like 'unknown', 'not recorded', and 'Unavailable' classified as Declared. That upgraded time_basis to Utc, raised reduce_confidence to High, and allowed the TimeOnlyCandidate ESP join the module contract refuses when the timezone is unrecognizable (ADR-002: timestamp proximity alone never creates strong correlation). Every Windows time zone identifier ends in 'Time', so the shape check anchors on that suffix; UTC and offset forms were already covered by utc_offset_re. Also makes detect_document's malformed detail a stable reducer-authored sentence: serde_json does not guarantee its error Display output across releases, and the detail flows into golden-asserted finding summaries. Refs #362, PR #450. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The module contract promises every whole-value mask is computed over the trimmed, lowercased value, but four sites called stable_token on the raw value: the ESP matched keys, sensitive named-data values, and both conflict-value shapes. A sensitive value arriving in a different casing or with surrounding space therefore masked to a different token than the identity field it should visibly equal, destroying the cross-field correlation the projection exists to preserve (ADR-004: same-scope redaction preserves intended equality). All four sites now go through mask_value, which also owns the is_token idempotency check. Also corrects the opaque-blob doc comment (the regex bound is 40, not 32) and pins the deliberate HRESULT canonicalization in normalize.rs: a 64-bit sign-extended token derives its canonical 32-bit hex while the raw token survives verbatim, and a genuinely 64-bit value gains no fabricated 32-bit form. Refs #362, PR #450. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…utopilot New inline scenarios pin the hardened invariants: non-assessable success and identity records cannot prove progress or raise the phase (ADR-001), a Conflicting ESP linkage reduces to ContradictoryEvidence with an explaining finding (ADR-003), a schema-version-only unknown capture is explained, and the native-event input path derives its artifact from the event's own provenance. Fixture corrections, each documented in its expected.json assertions: - malformed-report-section now carries a correctly tagged report document with a mangled sections payload, so it exercises the report-payload contract instead of generic non-JSON rejection, and its golden no longer pins serde_json's unstable error text. - unknown-windows-schema-version's summary now names the unvalidated schema version alongside the build. - completed-without-esp-bundle's evidence comment no longer claims a happy path, and the user-driven manifest describes local-phase completion only; post-handoff state belongs to ESP. update_findings_golden is now #[ignore]d so it never rewrites goldens beside the tests reading the same files, and write_json goes through a temp file plus rename so a reader can never observe a truncated golden. Refs #362, PR #450. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAutopilot parsing now uses assessability-aware evidence reduction, stable parsing details, normalized redaction, conservative ESP linkage, schema validation findings, and expanded integration coverage. The changes also update fixture metadata and CodeRabbit review settings. ChangesAutopilot assessment hardening
Estimated code review effort: 5 (Critical) | ~90+ minutes Sequence Diagram(s)sequenceDiagram
participant AutopilotSources
participant AutopilotReducer
participant ESPLinkage
participant FindingsPipeline
AutopilotSources->>AutopilotReducer: parsed observations and report sections
AutopilotReducer->>ESPLinkage: assessable and non-assessable correlation keys
ESPLinkage->>AutopilotReducer: proven, time-only, or conflicting linkage
AutopilotReducer->>FindingsPipeline: reduced evidence and conservative outcome
FindingsPipeline->>FindingsPipeline: emit schema, linkage, and non-assessable findings
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Consolidated multi-agent review (triage verification + gate-regression analysis, findings adversarially verified)Verdict: the triage is substantially honest and the two headline fixes are real — but the new assessability gating overcorrects in two places that must be fixed before merge, because they can now suppress failure evidence. Triage verification
Must-fix before merge (confirmed against the branch)
Should-fix
Clean checks (verified)
Review produced by 2 parallel read-only reviewers (triage verification incl. all 20 PR #450 threads; gate-regression/inverse analysis) with load-bearing findings re-verified against the branch. Refs #362, PR #450, ADR-001..004. |
… Autopilot success The assessability gate in sections_of hid every non-assessable report section from every consumer, including the Failed probes in reduce_outcome. A capped or unparsed profileApplication section whose outcome was failed became invisible, and sibling assessable evidence could then complete the enrollment at high confidence over a failure that was on record. The gate is now direction-aware (ADR-001 cuts both ways): sections_of still admits only assessable sections, so nothing non-assessable can prove progress or a terminal cause, and the new recorded_non_assessable_failure_sections iterator carries an explicitly recorded Failed/Mismatch outcome to the success branch of reduce_outcome, which then returns InsufficientEvidence instead of Completed/HandoffReachedEspEvidenceMissing. The recorded failure is never silent: the new autopilot-non-assessable-failure-recorded finding (low confidence, warning) cites the section and asks for a readable re-collection. NotFound and Retrying stay gated in both directions on purpose: those are absence or transient statements, and absence in a partial capture proves nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tection autopilot_keys gated every key on assessability, so a key carried only by a capped observation vanished entirely. When that key was the one binding a second ESP session, matched_sessions shrank from two to one and Conflicting collapsed into Linked into Completed: a non-assessable record silently upgrading the conclusion. Keys are the one input where more evidence is more conservative: every additional key can only widen the entangled-session set. So the key set is now split by AutopilotKeyGate. Proving keys (assessable only) are still the only ones that can produce Linked and the Completed outcome behind it; Detecting keys (all observations) feed the multi-session conflict check, which runs before any positive linkage and returns Conflicting naming every detected session. A linkage whose only key rides a non-assessable observation stays NotObserved/TimeOnlyCandidate, pinned by its own test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n its own The masking loop at the matched_keys site of the redacted export was behaviorally a no-op because autopilot_keys lowercases every key value first, and nothing tested either half of that coincidence. Both halves are now a contract: the reducer hands the projection lowercase values, and the projection masks a mixed-case value of the same key to the same token even though the reducer never produces one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four coordinated corrections from the PR #531 consolidated review: - distinct_values now groups case-insensitively with a deterministic representative casing (lexicographically smallest sighting). Serials and GUIDs are case-insensitive identities, and the redacted export masks the trimmed lowercased value, so a case-only difference exported as '2 distinct values' over two identical tokens -- a conclusion that changed under redaction (ADR-004). A case-only difference is no longer a conflict; genuinely distinct values still are, both pinned. - The shared-identifier next_evidence_request now survives the time gate: it is keyed on 'ESP facts supplied but not explicitly linked' (NotObserved or TimeOnlyCandidate) instead of on TimeOnlyCandidate alone, so narrowing the assessable overlap window can no longer erase the one step that advances the diagnosis. - classify_timezone: the windows_zone_re comment no longer claims every Windows zone id ends in 'Time' (UTC/UTC+12 are registry ids too, and localized StandardNames only degrade conservatively), and a small case-insensitive placeholder denylist (Local Time, Device Local Time, System Time) closes the placeholders the suffix anchor let through. - push_esp_link_conflicting acknowledges the legitimate two-real- sessions case: a reimaged or re-enrolled device provisions more than once, so the finding now says which situation the reader may be in and recommends per-attempt analysis before re-collection. Also appends the 153/172 application-evidence reasoning to the matching-autopilot-and-esp-session golden's assertions, the standard the PR sets for contested goldens. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…utopilot success The direction-aware assessability gate from the previous round covered report SECTIONS only. A capped or raw EVENT carrying a documented failure signal was still silently dropped by the is_assessable filters, so a bundle with an assessable success path (161 + 153-into-Available + an assessable espHandoff section) plus a capped event 172 reduced to a success-family outcome with no finding naming the failure. The class covers every documented failure signal: 171, 172, 807, 809, 815, 908. The event side now mirrors the section pattern exactly. The new recorded_non_assessable_failure_observations iterator selects the non-assessable observations whose signal is_terminal_failure -- the symmetry rule being that any record strong enough to fail the enrollment when readable is strong enough to block its success when unreadable -- and the success branch of reduce_outcome short-circuits to InsufficientEvidence over either record shape, never a terminal failure (ADR-001 cuts both ways). ProfilePolicyNotFound (100) stays out for the same reason the section iterator excludes NotFound/Retrying: documented transient, not a recorded failure. push_non_assessable_failure_recorded widens to cite both shapes, so the recorded failure is never silent; an assessable event 172 still produces the terminal ProfileApplicationFailure untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/reducer.rs (1)
715-745: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPreserve case-sensitive values when grouping distinct values.
Line 738 lowercases every named value.
distinct_valuesalso serveshardwareHash, whose Base64 representation is case-sensitive. Two different hashes that differ only by case collapse into one group.single_valuecan then publish one hash as corroborated identity evidence.Normalize only keys with a verified case-insensitive identity contract. Keep exact values for
hardwareHashand other case-sensitive fields. Add a regression test with two hardware hashes that differ only by case.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/reducer.rs` around lines 715 - 745, Update distinct_values to use case-insensitive grouping only for keys with a verified case-insensitive identity contract, while preserving exact trimmed values for hardwareHash and other case-sensitive fields. Ensure representative selection and evidence grouping remain deterministic, and add a regression test covering two hardware hashes that differ only by case so they remain distinct.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/reducer.rs`:
- Around line 1347-1353: Update the Conflicting AutopilotEspLinkage construction
to set matched_keys to an empty vector instead of collecting detected_keys,
while preserving detected_sessions and evidence. Do not alter the public type;
only expose detected keys through a separately versioned schema if that is
already required.
---
Outside diff comments:
In
`@crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/reducer.rs`:
- Around line 715-745: Update distinct_values to use case-insensitive grouping
only for keys with a verified case-insensitive identity contract, while
preserving exact trimmed values for hardwareHash and other case-sensitive
fields. Ensure representative selection and evidence grouping remain
deterministic, and add a regression test covering two hardware hashes that
differ only by case so they remain distinct.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 5701bf87-819f-486a-8965-4472234b9de9
📒 Files selected for processing (14)
crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/normalize.rscrates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/redaction.rscrates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/reducer.rscrates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/rules.rscrates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/sources.rscrates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/completed-without-esp-bundle/evidence/autopilot-channel/current/autopilot-events.jsoncrates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/completed-without-esp-bundle/manifest.jsoncrates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/malformed-report-section/evidence/mdm-diagnostics-report/current/autopilot-report.jsoncrates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/malformed-report-section/expected.jsoncrates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/malformed-report-section/manifest.jsoncrates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/matching-autopilot-and-esp-session/expected.jsoncrates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/unknown-windows-schema-version/expected.jsoncrates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/user-driven-success-through-esp-handoff/manifest.jsoncrates/cmtraceopen-parser/tests/intune_windows_autopilot.rs
…nsitivity Two CodeRabbit findings on the Conflicting linkage path and value grouping, both verified against the code before fixing: - The distinct-keys-to-distinct-sessions Conflicting return exported its detecting keys as matched_keys, but AutopilotEspLinkage documents matched_keys as empty for every non-Linked state. Detection is not a match; the keys stay internal so ambiguity evidence cannot be read as proof of a link. - distinct_values case-folded every named value, including hardwareHash, whose Base64 payload is case-sensitive. Two genuinely different hashes differing only by case collapsed into one group that single_value then published as corroborated identity. Case-insensitive grouping is now an explicit allowlist (CASE_INSENSITIVE_VALUE_KEYS) of verified case-insensitive Windows identities; every other key compares exactly, the conservative direction. All keys exported through detect_conflicts remain on the allowlist, preserving the ADR-004 redaction guarantee. Both fixes landed test-first: the new regression tests failed on the prior behavior for exactly the reported reasons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@coderabbitai approve |
✅ Action performedComments resolved. Approval is disabled; enable |
This branch forked before main's 96b841c enabled reviews.request_changes_workflow, and CodeRabbit resolves the config from the PR branch: the @coderabbitai approve command on PR #531 reported "Approval skipped: request-changes workflow disabled". Copy main's .coderabbit.yaml verbatim so the file carries zero net diff against main and the formal approval node can be produced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@coderabbitai approve |
✅ Action performedComments resolved and changes approved. |
Hermes charter reviewVerdictChanges requested — 3 blocking semantic findings remain. This is a charter review of PR #531 at exact head Findings (ranked)P1 — Non-assessable TPM failure is still emitted as a high-confidence blockerLocation:
Concrete failing input: an event 171 with Disposition: Open / blocking. The finding-side evidence helper needs the same assessability boundary, with a regression test proving that the non-assessable event cannot produce a high-confidence terminal finding. The assessable event-171 behavior should remain covered separately. P1 — Base64 hardware hashes can remain exposed by free-text redactionLocation:
Concrete cases: a 40-character value ending in Disposition: Open / blocking under the redaction-scope contract. Add regression cases for values ending in P1 — Unlinked failure-to-success observations can be silently resolved as successLocation:
Concrete failing input: assessable event 815 (no profile assigned), followed by assessable 161 and 153 ( Disposition: Open / blocking. Define the workload-local retry/session rule and encode it in adversarial tests before allowing the success transition to erase the earlier explicit negative. Do not infer chronology from vector order. Contract layer
Adversarial layerThe following attacks were checked against the exact-head source: non-assessable success and recorded-failure section/event paths; capped correlation keys widening ESP conflict detection without proving linkage; ambiguous multi-session linkage and Mechanical layer
Gates (observed at head)
Dispositions of prior-round feedback
Coverage statementThis review read the charter and routing indexes first, then the repository specialist context, ADR-001..004, reducer checklist, reviewer charters, the PR body/history, the exact Final state: CI PASS; CodeRabbit approved at head; contract conformance FAIL; Hermes review posted with 3 blocking P1 findings; PR #531 is not merge-ready. |
Address the three blocking semantic findings from the Hermes charter review of PR #531, each TDD'd (RED first with Hermes's exact inputs). 1. ADR-001 finding side: `signal_evidence` (rules.rs) now requires `is_assessable`, so a capped/malformed event 171 can no longer promote `autopilot-identity-registration-mismatch` to a High/Blocker finding the reducer's own outcome gate already withholds. The assessability check is hoisted to a single `AutopilotObservation::is_assessable` method shared by the reducer's free fn and the finding-side helpers so the boundary cannot drift. Assessable-171 still fires (regression pinned). 2. ADR-004 redaction: `opaque_blob_re` drops its `\b` anchors. The Base64 alphabet includes `=`, `+`, `/` (non-word), so a trailing `\b` could not close a match on a padded/punctuation-terminated hardware hash and left its tail exposed. The greedy, leftmost, >=40 match now consumes the whole contiguous run while still excluding the 36-char GUID and short HRESULT, and the `[blob:...]` token cannot re-match (idempotent). 3. ADR-003 chronology: `reduce_profile` no longer lets an unlinked later success (161/153) silently erase an earlier explicit negative (815/809). Positive evidence and explicit negatives are tracked apart and reconciled from sets (never vector order); a negative is erased only when it shares an `activityId` retry-linkage key with an Available-raising success. Unlinked stays conservative (NoProfileCandidate, not Completed); linked completes. Gates: cargo test -p cmtraceopen-parser (0 failures), clippy -D warnings, cargo check --workspace, wasm32 check all clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@/private/tmp/claude-502/-Users-Adam-Gell-repo-cmtraceopen/95f00e8d-9b26-48f6-ab69-f2216059e3f3/scratchpad/hermes_reply.md |
|
@coderabbitai review |
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/reducer.rs`:
- Around line 1009-1017: Update the doc comment for section_activity_id to state
that it returns the activityId value verbatim and does not lowercase it; note
that normalization is performed by push_link instead.
In `@crates/cmtraceopen-parser/tests/intune_windows_autopilot.rs`:
- Around line 1886-1894: Strengthen the redacted_export_projection test around
the existing text assertion by also verifying that the distinctive
repeated-character run from the Base64 blob is absent from the serialized
output. Keep the full-blob assertion and add a separate check that would fail if
redaction leaves trailing “=”, “+”, or “/” characters.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f820bf4f-9f05-460a-a6d8-fd63091845a9
📒 Files selected for processing (5)
crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/models.rscrates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/redaction.rscrates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/reducer.rscrates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/rules.rscrates/cmtraceopen-parser/tests/intune_windows_autopilot.rs
Hermes charter re-review (head 3d2ee13)Reviewed PR #531 at exact head Prior blocking findings
Specific implementation scrutiny(a) Assessability boundary: PASS. The shared method is defined on (b) Retry-linkage identity: PASS, with workload-local scope.
One limitation noted: the new retry regression matrix exercises event Contract and adversarial verdict
Gates observed at head
Clear statement: The three blocking findings from the prior Hermes review are genuinely closed at head This review covered the charter, routing indexes, reducer ADRs/checklist, exact-head Autopilot implementation and tests, PR gate state, all three prior findings, and the two requested implementation choices. It did not claim native Windows/lab validation. |
…b test CodeRabbit's two trivial findings on the Hermes P1 round, both valid. The section_activity_id doc claimed it lowercased its return value; it returns verbatim and push_link owns the lowercasing, so the doc now says where the normalization lives. The Base64 export test asserted only that the whole blob was absent, which would have passed while a masked body left its punctuation tail behind - the exact partial match the old word-boundary pattern produced - so it now asserts the body and the dangling tail are gone too. Verified: cargo test -p cmtraceopen-parser 2150 passed 0 failed (autopilot suite 47 passed), clippy -D warnings clean. Refs #362 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Hermes confirmation (head ae6e2ac) Clearance carries to this head. The delta from 3d2ee13 to ae6e2ac is doc/test-only: the reducer change updates a comment, and the test change strengthens the Base64 export assertion. The added body check catches a partially redacted blob, and the immediate punctuation-tail check catches a dangling =, +, or / after the mask token; the test therefore cannot pass with the tail leak described in the prior P1. I re-checked the exact-head branch read-only via git show. The three prior Hermes P1 findings remain closed: non-assessable event-171 evidence is excluded from terminal findings, opaque Base64 runs are fully masked, and unlinked profile failure-to-success transitions remain conservative and require explicit activity linkage. |
There was a problem hiding this comment.
Pull request overview
This PR hardens the Intune Windows Autopilot reducer by applying stricter “Reducer Framework v1” discipline to assessability gating, ESP linkage conflict handling, and redaction stability—then pins the corrected behavior with expanded tests and refreshed fixture expectations.
Changes:
- Tightens assessability gates (including direction-aware “recorded failure blocks success” behavior) across reducer paths and finding derivation.
- Improves ESP linkage logic to conservatively detect multi-session conflicts (including via non-assessable key carriers) and to avoid silent upgrades to
Completed. - Stabilizes redaction and parse-failure messaging (removing dependence on unstable
serde_jsonerror display text) and expands regression coverage.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/cmtraceopen-parser/tests/intune_windows_autopilot.rs | Adds targeted inline invariant tests (assessability, linkage conflicts, redaction contracts) and changes golden regeneration behavior. |
| crates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/user-driven-success-through-esp-handoff/manifest.json | Clarifies scenario description to reflect Autopilot-vs-ESP boundary. |
| crates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/unknown-windows-schema-version/expected.json | Updates assertions and unknown-schema finding summary to explain withheld semantics more completely. |
| crates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/matching-autopilot-and-esp-session/expected.json | Documents the reasoning for treating event 153 into ProfileState_Available as application evidence. |
| crates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/malformed-report-section/manifest.json | Updates metadata (bytesCopied) to match the corrected fixture payload. |
| crates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/malformed-report-section/expected.json | Replaces unstable serde error text with stable reducer-authored messaging; updates scenario assertions. |
| crates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/malformed-report-section/evidence/mdm-diagnostics-report/current/autopilot-report.json | Switches from XML to a tagged JSON envelope with a mangled sections payload to exercise the intended contract. |
| crates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/completed-without-esp-bundle/manifest.json | Updates metadata (bytesCopied) after fixture content adjustment. |
| crates/cmtraceopen-parser/tests/fixtures/intune/enrollment/windows/autopilot/completed-without-esp-bundle/evidence/autopilot-channel/current/autopilot-events.json | Corrects fixture comment to describe the “handoff reached but no ESP evidence captured” scenario. |
| crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/sources.rs | Stabilizes malformed-document detail and tightens timezone classification (Windows zone shape + placeholder denylist). |
| crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/rules.rs | Adds missing findings (ESP linkage conflicting; non-assessable failure recorded), and fixes unknown-schema explanation coverage. |
| crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/reducer.rs | Implements direction-aware assessability gating, improves linkage conflict detection/proof separation, stabilizes payload parse errors, and makes profile retry linkage order-invariant. |
| crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/redaction.rs | Fixes Base64 blob matching edge cases, and enforces consistent whole-value normalization/masking at all sites. |
| crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/normalize.rs | Adds regression test covering sign-extended HRESULT normalization semantics. |
| crates/cmtraceopen-parser/src/intune/enrollment/windows/autopilot/models.rs | Centralizes assessability logic on AutopilotObservation::is_assessable to prevent reducer/finding drift. |
| .coderabbit.yaml | Changes CodeRabbit review automation behavior (request-changes workflow, auto-labels, draft reviews). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| Err(_) => { | ||
| return AutopilotDocumentDetection::Malformed { | ||
| detail: format!("content is not a JSON object: {error}"), | ||
| detail: "content is not a JSON object".to_owned(), | ||
| } | ||
| } |
| let temporary = path.with_extension("json.tmp"); | ||
| std::fs::write(&temporary, text) | ||
| .unwrap_or_else(|error| panic!("{} is writable: {error}", temporary.display())); | ||
| std::fs::rename(&temporary, path) | ||
| .unwrap_or_else(|error| panic!("{} is replaceable: {error}", path.display())); |
| profile: assertive | ||
| # Keep reviews advisory: CI gates (cargo clippy, cargo test, tsc) decide mergeability. | ||
| request_changes_workflow: false | ||
| request_changes_workflow: true | ||
| high_level_summary: true | ||
| changed_files_summary: true |
Applies Reducer Framework v1 review discipline (docs/superpowers/plans/2026-08-07-reducer-framework-v1.md, ADR-001..004) to the merged Autopilot lane. Input was the full set of PR #450 review threads; every finding was verified against the merged code, then fixed, rejected with reasoning, or deferred with reasoning. Each real defect got a failing test before its fix, and every corrected golden documents in its
assertionswhy the prior expectation was unsafe.Refs #362. Follow-up to PR #450.
Triage of all 20 review findings
reduce_profileskips the assessability gate (reducer.rs)retrieved/appliedand reachCompletedwhile the failure branch was filtered. Gated, plus class audit below.IdentityObserved.Completed(reducer.rs)AutopilotConflict, so the empty-conflicts gate let it fall through toCompleted.reduce_outcomenow matchesConflictingexplicitly and returnsContradictoryEvidence; a newautopilot-esp-link-conflictingfinding explains exactly the previously-silent path.matched_keysmasking skips trim/lowercase normalization, 4 sites (redaction.rs)mask_value; regression test covers mixed case/whitespace across named data, conflict values (both shapes), matched keys, and identity fields.UnknownSchemawith no finding (rules.rs)autopilot_schema_version; summary names whichever declared value failed validation.windows_zone_reaccepts placeholder junk as a declared timezone (sources.rs)unknown/not recorded/Unavailableclassified asDeclared, upgrading time basis and enabling the forbidden time-only ESP candidate. Regex now anchors on theTimesuffix every Windows zone id ends with.update_findings_goldenis now#[ignore]d (run alone via-- --ignored), andwrite_jsonwrites through a temp file + rename. Doc comments updated.sectionspayload, exercisingabsorb_payload's malformed branch. Documented in the scenario'sassertions.serde_jsonerrorDisplaytextDisplaystability); raw bytes are still retained as evidence.completed-without-esp-bundleuser-driven-successmanifest claims a complete deploymentdetect_conflictscoverage vssingle_valuedeviceNamelegitimately change mid-provisioning, so promoting everysingle_valuekey to a conflict would fabricate contradictions.absorb_native_event: artifact derived from the event's own provenance, no document report, no coverage entry.profileAppliedset by event 153ProfileState_Availableis not application evidenceAvailableis its explicit success record. Reasoning now recorded as a comment at the site inreduce_profile.#[serde(default)]toNormalizedWindowsEvent.event_versionor v1 docs failOption<T>field asNonewithout any attribute. Empirical proof: the module doctest omitseventVersionentirely and passes in CI and locally.error_code_from_tokenloses the upper 32 bits of 64-bit HRESULTs0xFFFFFFFF80070002is0x80070002sign-extended through a 64-bit register; canonicalizing the derived hex to 32 bits is the intended normalization, the raw token survives verbatim, and a genuinely 64-bit value gains no fabricated 32-bit hex. Behavior is now pinned bya_sign_extended_hresult_canonicalizes_to_its_32_bit_form.#[non_exhaustive]microsoft_store(10 public enums),compliance(13),esp, and the rest of the Intune family use zero#[non_exhaustive](the only use in the crate is one SCCM type). The family's additivity strategy is the raw-preserving string-enum macro (unknown wire values survive verbatim inUnknown(String)without a new variant) plus the snapshotschema_versiondiscipline documented in models.rs. Diverging on one lane would break exhaustive matching for in-repo consumers without protecting anything the macro does not already protect. If the crate later adopts#[non_exhaustive], it should be a family-wide decision, not an Autopilot-only one.NormalizedWindowsEvent.event_versionis a breaking API change needing a version bump + Rustdocnormalized.rsis a shared model owned by the parser-family skeleton and is explicitly out of scope for this lane-hardening PR (no shared-model changes). The field already shipped in merged main; releasing it under the crate's versioning process is a release-management concern for the family owner, not something to relitigate from one lane.unknown-windows-schema-versioncannot exercise the schema-only gatea_schema_version_only_unknown_schema_is_explained_by_a_finding(windows build undeclared, schema version 3) rather than by widening the pinned 15-scenario fixture matrix, which issue #362 treats as a contract.malformed-report-section,unknown-windows-schema-version) document in theirassertionswhy the prior expectation was unsafe.Class audit: which reduction paths gained the assessability gate
Beyond the two named findings, every reduction path in the module was audited (close the class, not the instance):
reduce_profilemain observation loop — gained the gate (Major finding)reduce_identityIDENTITY_KEYS evidence loop — gained the gate (Minor finding)sections_of— gained a section-level gate (is_assessable_section), covering every report-section path at one enforcement point: identity sections, profile retrieval/application sections, handoff sections, and all fourreduce_outcomesection probesdetect_conflictsmismatch-section loop — gained the gate (an unreadable section cannot assert a mismatch)autopilot_keyscorrelation-key extraction — gained the gate (a non-assessable record cannot mint aLinkedcorrelation)reduce_esp_linkageevidence-mention loop — gained the gatehas_time_overlap— gained the gate (non-assessable timestamps cannot create aTimeOnlyCandidate)signal_observations/has_signal/distinct_values(hencesingle_value) — already gatedtime_basis— deliberately left unfiltered and documented: there the unfiltered scan is the conservative direction (a non-assessable record with an unnormalized timestamp downgrades the basis; gating it would upgrade it)Gates (exact commands, run at head)
Golden regeneration was run in isolation exactly once (
UPDATE_AUTOPILOT_FINDINGS=1 cargo test --test intune_windows_autopilot -- --ignored update_findings_golden) and the diff reviewed; only the two summaries listed above changed.Fix round (consolidated multi-agent review)
All three must-fixes and all five should-fixes from the consolidated review are addressed at head. TDD throughout: every behavioral item landed as a failing test first.
Must-fix 1, direction-aware assessability gate (
a4d0daeb).sections_ofstays assessable-only for everything that can prove progress or a terminal cause; the newrecorded_non_assessable_failure_sectionsiterator routes an explicitly recordedfailed/mismatchoutcome on a capped/unparsed section to the success branch ofreduce_outcome, which now returnsInsufficientEvidenceinstead ofCompleted/HandoffReachedEspEvidenceMissing. Rationale documented in-code: ADR-001 forbids terminal conclusions from non-assessable evidence in both directions, so the failure outcome itself stays reserved for assessable records and the honest reduction isInsufficientEvidence(Low confidence), never a silent success. Surfaced by the newautopilot-non-assessable-failure-recordedfinding (warning, low confidence, cites the section, asks for a readable re-collection).NotFound/Retryingstay gated in both directions: absence statements from a partial capture prove nothing. Fixtures:a_recorded_failure_on_a_non_assessable_section_blocks_success(assessable success path + cappedprofileApplicationfailed section, the exact scenario the review named), plus the negative controlan_assessable_failed_section_still_produces_the_terminal_failure.Must-fix 2, correlation keys survive for conflict detection (
6c031f92).autopilot_keysis now gated byAutopilotKeyGate::{Proving, Detecting}: proving keys (assessable only) remain the only ones able to produceLinkedand theCompletedbehind it; detecting keys (all observations) feed a multi-session conflict check that runs before any positive linkage and returnsConflictingnaming every detected session. More sessions detected means more conservative; a non-assessable-only match can never upgrade pastNotObserved/TimeOnlyCandidate. Fixtures:a_key_on_a_capped_observation_still_detects_a_second_esp_session(session B matched only via a capped observation, so no silent 2-to-1 collapse) anda_non_assessable_only_key_match_cannot_upgrade_to_linked.Must-fix 3,
matched_keysmasking pinned as a contract (cca27db8).matched_key_masking_normalizes_case_independently_of_the_reducerpins both halves:autopilot_keyshands the projection lowercase values, and theredaction.rsmasking loop still produces the same token when fed a mixed-case value it never normally receives.Should-fix 4, case-insensitive
distinct_values(5ce8e2e5). Decided for case-insensitivity: serials/GUIDs are case-insensitive identities and the export masks the trimmed lowercased value, so a case-only difference exporting "2 distinct values" over two identical tokens changed a conclusion under redaction (ADR-004). Groups now key on the lowercased value with a deterministic representative (lexicographically smallest sighting, permutation-safe per ADR-003). Tests:a_case_only_identifier_difference_is_not_a_conflict+ controlgenuinely_distinct_identifiers_still_conflict.Should-fix 5, triage bookkeeping (
5ce8e2e5). The 153/172 application-evidence reasoning is now in theassertionsarray of thematching-autopilot-and-esp-sessiongolden. Bookkeeping correction for the triage table: theopaque_blob_redoc-comment finding was fixed in this PR but was omitted from the table; row 20 being self-authored, the honest count is 19 externally reported + 1 self-reported.Should-fix 6, evidence request survives the time gate (
5ce8e2e5). The shared-identifiernext_evidence_requestis now keyed on "ESP facts supplied but not explicitly linked" (NotObservedorTimeOnlyCandidate), so the narrowed assessable overlap window can no longer erase the guidance. Test:unlinked_esp_sessions_keep_the_shared_identifier_evidence_request.Should-fix 7,
windows_zone_re(5ce8e2e5). Comment corrected (no more "every Windows id ends in Time";UTC/UTC+12are registry ids handled byutc_offset_re, and localized StandardNames degrade conservatively) and a case-insensitive placeholder denylist (Local Time,Device Local Time,System Time) closes the hole the suffix anchor left. Tests extended insources.rs.Should-fix 8, multi-session finding text (
5ce8e2e5).push_esp_link_conflictingnow acknowledges the legitimate reimage/retry two-real-sessions case and recommends per-attempt analysis before re-collection.Fixture note: the two new adversarial scenarios are built inline in
tests/intune_windows_autopilot.rs(the file's documented pattern for single-invariant ADR tests) because the issue #362 fixture-directory matrix is pinned as a contract; the corpus-level gap the review flagged (noaccessState != available/parseState != parsedinput anywhere in CI) is closed by these tests.Gates at head:
cargo test -p cmtraceopen-parser→ 2139 passed, 0 failed, 1 ignored;cargo clippy -p cmtraceopen-parser --all-targets -- -D warnings→ clean;cargo check --workspace→ clean;cargo check --locked -p cmtraceopen-parser --target wasm32-unknown-unknown→ clean.Ultrareview round, event-side closure of the assessability gate (
874d322b). The direction-aware gate above covered report SECTIONS only; a capped or raw EVENT carrying a documented failure signal was still silently dropped by theis_assessablefilters, so an assessable success path (161 + 153-into-Available + assessable espHandoff section) plus a capped event 172 reduced to a success-family outcome with no finding. Closed as a class: the newrecorded_non_assessable_failure_observationsiterator gates onAutopilotSignal::is_terminal_failure(171, 172, 807, 809, 815, 908 -- the symmetry rule being that any signal strong enough to fail the enrollment when readable is strong enough to block its success when unreadable; 100 stays out as documented-transient, matching the section iterator'sNotFound/Retryingexclusion), and the success branch ofreduce_outcomeshort-circuits toInsufficientEvidenceover either record shape.push_non_assessable_failure_recordedwidens to cite both shapes, so the recorded failure is never silent. TDD:a_recorded_failure_on_a_non_assessable_event_blocks_successobserved red first (reduced toHandoffReachedEspEvidenceMissing, no finding), then green; symmetric controlan_assessable_failed_event_still_produces_the_terminal_failurepins that an assessable 172 still yields the terminalProfileApplicationFailure.Gates at head:
cargo test -p cmtraceopen-parser-> 2141 passed, 0 failed, 1 ignored;cargo clippy -p cmtraceopen-parser --all-targets -- -D warnings-> clean;cargo check --workspace-> clean;cargo check -p cmtraceopen-parser --target wasm32-unknown-unknown-> clean.Hermes review fixes (
3d2ee13d)The Hermes charter review posted three blocking P1 findings against exact head
ed8b50b9. Each is now closed, TDD'd RED-first with the review's exact inputs.P1 #1, ADR-001 finding-side assessability (
rules.rs).signal_evidencefiltered only by signal, so a capped/malformed event 171 (TpmIdentityFailed) could still promoteautopilot-identity-registration-mismatchto aBlocker/Highfinding even thoughreduce_outcomecorrectly withheld the terminal outcome.signal_evidencenow requiresis_assessable, and the check is hoisted to oneAutopilotObservation::is_assessablemethod shared by the reducer's free fn and the finding-side helpers so the boundary cannot drift. The recorded failure stays visible via the existing low-confidenceautopilot-non-assessable-failure-recorded. TDD:a_non_assessable_tpm_failure_cannot_emit_a_high_confidence_blocker(red first); regression controlan_assessable_tpm_failure_still_emits_the_identity_mismatch_blocker.P1 #2, ADR-004 free-text redaction (
redaction.rs).opaque_blob_rewas\b[A-Za-z0-9+/=]{40,}\b. The Base64 alphabet includes=,+,/, none of which is a word character, so the trailing\bcould not close the match on a padded or punctuation-terminated hardware hash and left its tail exposed (a 40-char value ending in//+matched nothing at all). The pattern drops both anchors; the greedy, leftmost,>=40match now consumes the whole contiguous Base64 run in full while the>=40bound still excludes the 36-char GUID (dashes break it into<=12-char runs) and the short HRESULT, and the[blob:…]token cannot re-match (idempotent). GUID/HRESULT preservation tests still pass. TDD:a_base64_blob_ending_in_punctuation_is_masked_in_full(unit, red first) plusa_base64_hash_in_an_observation_message_never_survives_the_exportpinning the whole exported projection for=,==,+,/.P1 #3, ADR-003 profile retry linkage (
reducer.rs).reduce_profilelet a later success (161/153) silently overwrite an earlier explicit negative (NoAssignedProfile815 /AssignedProfileMissing809) with no retry linkage, and the result depended on vector order. Positive evidence and explicit negatives are now tracked apart and reconciled from sets (never vector order): a negative is erased only when it shares anactivityIdretry-linkage key (the module's session/correlation key, matched case-insensitively) with anAvailable-raising success. Without linkage the negative stands (NoProfileCandidate, notCompleted); with linkage the success completes. TDD:an_unlinked_success_cannot_erase_an_earlier_no_profile_negative(red first), linkage-permitted controla_retry_linked_success_completes_over_an_earlier_negative, and input-order-permutation assertionthe_profile_linkage_verdict_is_invariant_under_input_order.Retry-linkage key used:
AutopilotObservation.activity_id(and the report section'sactivityIdvalue), lowercased -- the same key the ESP correlation path already treats as a session identity.Gates at head:
cargo test -p cmtraceopen-parser-> 2150 passed, 0 failed, 1 ignored;cargo clippy -p cmtraceopen-parser --all-targets -- -D warnings-> clean;cargo check --workspace-> clean;cargo check -p cmtraceopen-parser --target wasm32-unknown-unknown-> clean.🤖 Generated with Claude Code
Summary by CodeRabbit