Skip to content

fix(installcoord): name drifted guidance assets with content drift first - #435

Merged
barbatdev merged 1 commit into
mainfrom
fix/384-guidance-drift-core
Nov 12, 2026
Merged

barbatdev merged 1 commit into
mainfrom
fix/384-guidance-drift-core

Conversation

@barbatdev

Copy link
Copy Markdown
Contributor

Pull Request

Linked Issue

Refs #384

PR Type

  • type:feature Feature
  • type:bug Bug fix
  • type:docs Documentation
  • type:refactor Refactoring
  • type:chore Maintenance
  • type:breaking-change Breaking change

Summary

First of two PRs that replace #383, which grew to 661 lines, over the 400-line review budget. This one holds the installcoord core: guidance evidence names each drifted asset with its cause, and content drift takes precedence over mode drift (the #384 finding). Mode drift stays an independent flag, consistent with the conflict path.

Changes

Focused Tests

Claim Test command Observed result Limit State Observed by
Content plus mode drift reported only mode drift Package test before the precedence fix Cause:0x2 ... want cause 1 Verified Agent that made the change
Content drift wins, mode stays a flag go test ./internal/installcoord -count=1 ok Package API; the CLI line is tested in the follow-up PR Verified Agent that made the change and parent re-run
Mode-only and content-only cases unchanged Same package tests Pass Verified Agent that made the change
Full suite on main with this branch go vet ./...; go test ./... -count=1 Clean; 35 packages ok Verified Parent

Not tested: the CLI output of the refusal; that surface lands in the follow-up PR.

Failed or not done: none.

Warnings: the second PR (CLI and docs) depends on this one. #383 will be closed as superseded once both are open.

Runtime Harness

N/A: package change covered by package tests.

Rollback

Revert the single commit; the follow-up PR must be reverted first if it has landed.

Chain Dependencies / Out of Scope

Checklist

  • Linked issue is open and status:approved.
  • Exactly one matching type:* label is applied.
  • Focused tests and runtime harness evidence are recorded.
  • Rollback and out-of-scope boundaries are documented.

@barbatdev barbatdev added the type:bug Bug fix label Nov 12, 2026
@barbatdev
barbatdev merged commit d16eeaa into main Nov 12, 2026
3 checks passed
@barbatdev

Copy link
Copy Markdown
Contributor Author

Fleet review (glm)

Lens: requirements + adversarial. The diff fixes the #384 blocking finding at its named location: recordedDriftAssets checks content drift (!owned || ownerHash != slot.SHA256) before ModeDrift, so a combined content+mode drift reports ActorGuidanceContentDrift with ModeDrift as the independent flag, and mode-only drift still reports ActorGuidanceModeDrift — mirroring the conflict-stage invariant. The combined evidence-stage case is pinned by a table case asserting cause, both modes, and ModeDrift=true; mode-only and content-only cases are pinned unchanged. The refusal record is detached (defensive copy verified by mutation test), a successful preflight carries no refusal, and refusals write nothing (tree-compared). Early evidence refusals on invalid units degrade to zero named assets rather than panicking or inventing paths. No blocking findings.

No findings.

Verdict: ready to merge

@barbatdev
barbatdev deleted the fix/384-guidance-drift-core branch November 12, 2026 21:03
@barbatdev

Copy link
Copy Markdown
Contributor Author

Post-merge QA (glm)

Lens: requirements + adversarial. The diff fixes the #384 blocking finding at its named location: recordedDriftAssets checks content drift (!owned || ownerHash != slot.SHA256) before ModeDrift, so a combined content+mode drift reports ActorGuidanceContentDrift with ModeDrift as the independent flag, and mode-only drift still reports ActorGuidanceModeDrift — mirroring the conflict-stage invariant. The combined evidence-stage case is pinned by a table case asserting cause, both modes, and ModeDrift=true; mode-only and content-only cases are pinned unchanged. The refusal record is detached (defensive copy verified by mutation test), a successful preflight carries no refusal, and refusals write nothing (tree-compared). Early evidence refusals on invalid units degrade to zero named assets rather than panicking or inventing paths. No blocking findings.

No findings.

Verdict: clean

@barbatdev

Copy link
Copy Markdown
Contributor Author

Post-merge QA (deepseek)

Lens: evidence + test design. The merged code implements the #384 acceptance criterion at the right level: recordedDriftAssets now evaluates content drift first (!owned || ownerHash != slot.SHA256) and keeps mode drift as the independent ModeDrift flag, with mode drift as the cause only when it is the sole drift, and TestPreflightActorGuidanceRefusalNamesCause adds the combined content+mode case plus the case where a conflicting skill also carries mode drift. The criterion's scenario (bytes appended by an external editor, mode 0640) is therefore reproduced as a package test, and the Refusal() record is detached (Assets() returns a copy) with the zero value meaning 'not refused'. Two evidence gaps: the documented no-recorded-ledger branch has no test that names it, and the early-return refusals reuse recordedDriftAssets even when the refusal is about the runtime matrix rather than the files. No blocking defect: I traced the refusal paths and the two cause values exhaustively, and none reports a wrong cause for the merged inputs.

Suggestion (1)

  • Runtime-matrix refusals now carry file drift that is not the reason for the refusal (internal/installcoord/actor_guidance.go:212): Every early return now calls recordedDriftAssets(unit), including the runtime-matrix branch (err != nil || !matrix.HasCompatible), which refuses because the local runtime is incompatible or disabled, not because a guidance file drifted. After the CLI half (feat(cli): name failing assets and drift cause on actor_guidance_unverified refusals #436) lands, that refusal prints asset=... cause=content_drift action=conflict lines for every drifted file next to a reason token about the runtime, so an operator fixing the runtime first sees file conflicts that were not the reason for the refusal. Either scope assets to the evidence stage or accept and document that the lines are informational, so a later reader does not treat them as the blocker.

Question (1)

  • The no-recorded-ledger branch is documented but not exercised by name (internal/installcoord/actor_guidance.go:285): recordedDriftAssets documents that "without any recorded ledger nothing can be verified as owned, so every present candidate asset is reported", a widening of the refusal compared with a ledger-present run. No test in the merged actor_guidance_test.go mentions a prior state or a nil ledger, so the behaviour that decides whether every present asset is named in that case is asserted only implicitly. Condition worth pinning: a unit whose observation has no prior state and whose plan has N assets, one refused preflight, expected refusal assets = the N present assets, all with cause content drift and an empty RelativePath never occurring.

Verdict: clean

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant