Skip to content

feat(wfctl): drift class enum + apply --refresh ghost-prune for production state recovery#519

Merged
intel352 merged 6 commits into
mainfrom
feat/apply-refresh-flag
May 2, 2026
Merged

feat(wfctl): drift class enum + apply --refresh ghost-prune for production state recovery#519
intel352 merged 6 commits into
mainfrom
feat/apply-refresh-flag

Conversation

@intel352
Copy link
Copy Markdown
Contributor

@intel352 intel352 commented May 2, 2026

Summary

Adds reusable wfctl drift-recovery primitives (first PR in a 4-PR drift-recovery chain):

  • interfaces.DriftClass enum + DriftResult.Class field (additive, backwards-compatible — omitempty on zero value preserves existing serialization)
  • wfctl infra apply --refresh flag prunes ghost-in-state entries (cloud 404s) before the normal plan+apply
  • wfctl infra apply --allow-protected-prune two-key flag for resources with protected: true in state Outputs
  • wfctl infra drift CLI output extended with Class column (GHOST / CONFIG / IN-SYNC)
  • Operator docs at docs/wfctl/drift-recovery.md

Production safety:

  • Dry-run by default when --refresh is set without --auto-approve
  • Audit log to stderr per prune (wfctl: state mutation prune <name> ...)
  • Transient DetectDrift API errors propagate without any state mutation
  • Protected resources require explicit --allow-protected-prune two-key

Test plan

  • go test ./interfaces/... green (4 DriftClass JSON marshaling tests)
  • go test ./cmd/wfctl/... green (6 TestApplyRefresh_* tests covering dry-run, auto-approve, protected-block, protected-with-flag, transient-error-propagation, in-sync-skip)
  • go test ./... green — zero failures across full suite
  • Manual: wfctl infra apply --help shows --refresh and --allow-protected-prune flags
  • Validation post-merge: exercised against core-dump staging via PR-D3 chain (lockfile bumps)

Sequencing

This is PR-D2 in the chain. After merge + release tag (v0.20.5):

  • PR-D1 (workflow-plugin-digitalocean feat/detect-drift-impl) rebases its go.mod onto this release to use interfaces.DriftClass* constants
  • PR-D3a (core-dump lockfile bump) + PR-D3b (BMW lockfile bump) follow

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

intel352 and others added 5 commits May 2, 2026 12:02
Add DriftClass string type with 4 constants:
- DriftClassUnknown (zero value, omitempty-safe for backwards compat)
- DriftClassInSync
- DriftClassGhost (state has resource; cloud returns ErrResourceNotFound)
- DriftClassConfig (both exist; configs differ)

Extend DriftResult with Class DriftClass json:"class,omitempty" field
(additive, backwards-compatible — consumers without the field see no
JSON change due to omitempty).

4 tests covering constant values, omitempty-on-zero, ghost JSON
rendering, and round-trip marshal/unmarshal for all 3 non-zero classes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…very

New function runInfraApplyRefreshPhase calls provider.DetectDrift and
prunes ghost-in-state entries (DriftClassGhost) from the state store:

- Dry-run by default (no autoApprove): prints "would prune" per ghost
- autoApprove=true: calls store.DeleteResource + emits audit log to stderr
- Protected resources blocked unless allowProtectedPrune=true
- Transient DetectDrift errors propagate immediately; no pruning happens
- DriftClassConfig / DriftClassInSync entries skipped (regular plan path)

6 tests covering: dry-run no-mutate, auto-approve prune, protected-block,
protected-with-flag, transient-error-propagation, in-sync-skip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…apply

Add two flags to runInfraApply:
- --refresh: runs runInfraApplyRefreshPhase before plan+apply, iterating
  all state-tracked provider groups via groupStatesByProvider and pruning
  any DriftClassGhost entries.
- --allow-protected-prune: passed to runInfraApplyRefreshPhase to permit
  pruning resources with protected:true in state Outputs.

Refresh phase only fires when --refresh is set and the config has infra.*
modules; silently skipped for legacy platform.* configs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
driftInfraModules now prints drift class (GHOST / CONFIG / IN-SYNC)
using the DriftClass constants from interfaces:

  GHOST    <name>   <type>   — cloud reports not found
  CONFIG   <name>   <type>
    <field>: expected=<v>  actual=<v>
  IN-SYNC  <name>   <type>

Providers still returning DriftClassUnknown fall through to the legacy
Drifted-bool behavior for backwards compatibility.

Column-aligned format matches wfctl infra status output style.
Drift-found message updated to suggest --refresh flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs/wfctl/drift-recovery.md (~100 lines) covering:
- Three drift classes (ghost / config / in-sync) with recovery actions
- wfctl infra drift usage + example output with Class column
- Dry-run-first workflow → auto-approve prune
- Protected resource two-key contract (--allow-protected-prune)
- Audit log format
- Production safety checklist
- CI integration patterns

CHANGELOG.md Unreleased section: DriftClass enum, --refresh flag,
--allow-protected-prune flag, drift output Class column, docs file.
Notes omitempty additions to DriftResult.Expected/Actual/Fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 2, 2026 16:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first set of wfctl drift-recovery primitives: a drift classification enum in interfaces, refreshed wfctl infra apply behavior to prune ghost-in-state entries, extended drift CLI output, and operator documentation.

Changes:

  • Add interfaces.DriftClass + DriftResult.Class (omitempty) and update DriftResult JSON tags.
  • Implement wfctl infra apply --refresh (with --allow-protected-prune) to prune ghost state entries before plan/apply.
  • Update wfctl infra drift output to show drift class; add docs and new tests; update changelog.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
interfaces/iac_provider.go Introduces DriftClass and adds Class + omitempty JSON tags to DriftResult.
interfaces/iac_provider_test.go Adds JSON/round-trip tests for DriftClass and DriftResult.Class.
cmd/wfctl/infra_apply_refresh.go New refresh phase implementation that detects drift and prunes ghost-in-state entries.
cmd/wfctl/infra_apply_refresh_test.go New unit tests covering refresh dry-run, prune, protected gating, and error propagation.
cmd/wfctl/infra.go Wires --refresh and --allow-protected-prune flags into infra apply.
cmd/wfctl/infra_status_drift.go Updates drift output to include class-based formatting and messaging.
docs/wfctl/drift-recovery.md Adds an operator runbook for drift detection and recovery workflows.
CHANGELOG.md Documents the new drift-recovery functionality under Unreleased.

Comment thread docs/wfctl/drift-recovery.md Outdated
Comment on lines +141 to +142
See `docs/plans/2026-05-02-infra-drift-recovery.md` for the full recovery
design rationale.
Comment on lines +60 to +67
isProtected := isRefProtected(states, r.Name)
if isProtected && !allowProtectedPrune {
// Hard-block: return an error immediately so the caller sees the
// problem. No prunes have happened at this point.
fmt.Fprintf(stderr, "wfctl: BLOCKED: %s is protected; cannot prune without --allow-protected-prune\n", r.Name)
return fmt.Errorf("refresh: blocked on protected resource %q (use --allow-protected-prune to override)", r.Name)
}

Comment thread cmd/wfctl/infra.go
Comment on lines +947 to +950
var refreshFlag bool
fs.BoolVar(&refreshFlag, "refresh", false, "Detect drift and prune ghost-in-state entries before applying")
var allowProtectedPruneFlag bool
fs.BoolVar(&allowProtectedPruneFlag, "allow-protected-prune", false, "Allow pruning state entries for resources marked protected: true (requires --refresh)")
Comment on lines +10 to +14
| Class | Description | Recovery |
|-------|-------------|----------|
| `ghost` | State says resource exists; cloud returns 404 | Prune state entry via `--refresh` |
| `config` | Both exist but configs differ (e.g. someone edited cloud-side) | Reconcile via normal `wfctl infra apply` |
| `in-sync` | State and cloud agree | No action needed |
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 2, 2026

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:245: parsing iteration count: invalid syntax
baseline-bench.txt:335538: parsing iteration count: invalid syntax
baseline-bench.txt:606994: parsing iteration count: invalid syntax
baseline-bench.txt:912350: parsing iteration count: invalid syntax
baseline-bench.txt:1255818: parsing iteration count: invalid syntax
baseline-bench.txt:1782094: parsing iteration count: invalid syntax
benchmark-results.txt:245: parsing iteration count: invalid syntax
benchmark-results.txt:343557: parsing iteration count: invalid syntax
benchmark-results.txt:660979: parsing iteration count: invalid syntax
benchmark-results.txt:1006724: parsing iteration count: invalid syntax
benchmark-results.txt:1323162: parsing iteration count: invalid syntax
benchmark-results.txt:1625326: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │       sec/op       │    sec/op      vs base              │
InterpreterCreation-4              3.726m ± 200%   3.191m ± 214%       ~ (p=1.000 n=6)
ComponentLoad-4                    3.566m ±   1%   3.610m ±  14%  +1.25% (p=0.004 n=6)
ComponentExecute-4                 1.917µ ±   1%   1.944µ ±   0%  +1.36% (p=0.002 n=6)
PoolContention/workers-1-4         1.075µ ±   4%   1.091µ ±   1%       ~ (p=0.084 n=6)
PoolContention/workers-2-4         1.075µ ±   4%   1.084µ ±   1%       ~ (p=0.474 n=6)
PoolContention/workers-4-4         1.082µ ±   1%   1.087µ ±   1%       ~ (p=0.167 n=6)
PoolContention/workers-8-4         1.083µ ±   1%   1.097µ ±   2%  +1.25% (p=0.009 n=6)
PoolContention/workers-16-4        1.094µ ±   1%   1.097µ ±   5%       ~ (p=0.193 n=6)
ComponentLifecycle-4               3.646m ±   1%   3.624m ±   2%       ~ (p=0.699 n=6)
SourceValidation-4                 2.257µ ±   0%   2.273µ ±   1%  +0.73% (p=0.017 n=6)
RegistryConcurrent-4               805.3n ±   4%   820.0n ±   6%       ~ (p=0.310 n=6)
LoaderLoadFromString-4             3.694m ±   1%   3.647m ±   2%       ~ (p=0.240 n=6)
geomean                            17.66µ          17.55µ         -0.66%

                            │ baseline-bench.txt │        benchmark-results.txt         │
                            │        B/op        │     B/op      vs base                │
InterpreterCreation-4               2.027Mi ± 0%   2.027Mi ± 0%       ~ (p=0.732 n=6)
ComponentLoad-4                     2.180Mi ± 0%   2.180Mi ± 0%       ~ (p=0.983 n=6)
ComponentExecute-4                  1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4         1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                2.183Mi ± 0%   2.183Mi ± 0%       ~ (p=0.667 n=6)
SourceValidation-4                  1.984Ki ± 0%   1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                1.133Ki ± 0%   1.133Ki ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4              2.182Mi ± 0%   2.182Mi ± 0%       ~ (p=0.584 n=6)
geomean                             15.25Ki        15.25Ki       +0.00%
¹ all samples are equal

                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │     allocs/op      │  allocs/op   vs base                │
InterpreterCreation-4                15.68k ± 0%   15.68k ± 0%       ~ (p=1.000 n=6)
ComponentLoad-4                      18.02k ± 0%   18.02k ± 0%       ~ (p=1.000 n=6)
ComponentExecute-4                    25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4           25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                 18.07k ± 0%   18.07k ± 0%       ~ (p=1.000 n=6) ¹
SourceValidation-4                    32.00 ± 0%    32.00 ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                  2.000 ± 0%    2.000 ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4               18.06k ± 0%   18.06k ± 0%       ~ (p=1.000 n=6) ¹
geomean                               183.3         183.3       +0.00%
¹ all samples are equal

pkg: github.com/GoCodeAlone/workflow/middleware
                                  │ baseline-bench.txt │       benchmark-results.txt       │
                                  │       sec/op       │   sec/op     vs base              │
CircuitBreakerDetection-4                  286.2n ± 3%   284.6n ± 3%       ~ (p=0.221 n=6)
CircuitBreakerExecution_Success-4          21.54n ± 0%   21.54n ± 0%       ~ (p=0.364 n=6)
CircuitBreakerExecution_Failure-4          66.25n ± 0%   65.87n ± 1%       ~ (p=0.058 n=6)
geomean                                    74.20n        73.91n       -0.40%

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │        B/op        │    B/op     vs base                │
CircuitBreakerDetection-4                 144.0 ± 0%     144.0 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │     allocs/op      │ allocs/op   vs base                │
CircuitBreakerDetection-4                 1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │       sec/op       │    sec/op     vs base              │
JQTransform_Simple-4                     871.5n ± 32%   866.6n ± 29%       ~ (p=0.394 n=6)
JQTransform_ObjectConstruction-4         1.452µ ± 25%   1.443µ ±  1%  -0.62% (p=0.015 n=6)
JQTransform_ArraySelect-4                3.345µ ±  1%   3.295µ ±  1%  -1.49% (p=0.002 n=6)
JQTransform_Complex-4                    38.82µ ±  1%   38.05µ ±  3%  -1.99% (p=0.041 n=6)
JQTransform_Throughput-4                 1.769µ ±  1%   1.767µ ±  0%       ~ (p=0.372 n=6)
SSEPublishDelivery-4                     70.12n ±  0%   70.37n ±  3%       ~ (p=0.240 n=6)
geomean                                  1.653µ         1.640µ        -0.74%

                                 │ baseline-bench.txt │        benchmark-results.txt         │
                                 │        B/op        │     B/op      vs base                │
JQTransform_Simple-4                   1.273Ki ± 0%     1.273Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4       1.773Ki ± 0%     1.773Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4              2.625Ki ± 0%     2.625Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                  16.22Ki ± 0%     16.22Ki ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4               1.984Ki ± 0%     1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²                 +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │       benchmark-results.txt        │
                                 │     allocs/op      │ allocs/op   vs base                │
JQTransform_Simple-4                     10.00 ± 0%     10.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4         15.00 ± 0%     15.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4                30.00 ± 0%     30.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                    324.0 ± 0%     324.0 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4                 17.00 ± 0%     17.00 ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
                                    │ baseline-bench.txt │       benchmark-results.txt       │
                                    │       sec/op       │   sec/op     vs base              │
SchemaValidation_Simple-4                   1.124µ ± 13%   1.124µ ± 5%       ~ (p=0.937 n=6)
SchemaValidation_AllFields-4                1.677µ ±  1%   1.662µ ± 2%       ~ (p=0.084 n=6)
SchemaValidation_FormatValidation-4         1.597µ ±  3%   1.577µ ± 3%       ~ (p=0.102 n=6)
SchemaValidation_ManySchemas-4              1.817µ ±  4%   1.829µ ± 3%       ~ (p=0.310 n=6)
geomean                                     1.529µ         1.523µ       -0.39%

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │        B/op        │    B/op     vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │     allocs/op      │ allocs/op   vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │       sec/op       │    sec/op     vs base               │
EventStoreAppend_InMemory-4                1.180µ ± 16%   1.217µ ± 35%        ~ (p=0.589 n=6)
EventStoreAppend_SQLite-4                  1.362m ±  5%   1.327m ±  5%   -2.58% (p=0.041 n=6)
GetTimeline_InMemory/events-10-4           13.58µ ±  3%   13.94µ ±  3%   +2.62% (p=0.004 n=6)
GetTimeline_InMemory/events-50-4           76.05µ ±  2%   77.86µ ±  2%   +2.39% (p=0.026 n=6)
GetTimeline_InMemory/events-100-4          152.2µ ±  5%   124.0µ ± 24%  -18.52% (p=0.026 n=6)
GetTimeline_InMemory/events-500-4          782.5µ ± 21%   636.0µ ±  1%        ~ (p=0.065 n=6)
GetTimeline_InMemory/events-1000-4         1.261m ±  1%   1.303m ±  1%   +3.32% (p=0.002 n=6)
GetTimeline_SQLite/events-10-4             104.3µ ±  0%   109.1µ ±  1%   +4.61% (p=0.002 n=6)
GetTimeline_SQLite/events-50-4             241.0µ ±  1%   250.6µ ±  1%   +3.96% (p=0.002 n=6)
GetTimeline_SQLite/events-100-4            408.2µ ±  1%   422.8µ ±  0%   +3.58% (p=0.002 n=6)
GetTimeline_SQLite/events-500-4            1.747m ±  0%   1.804m ±  0%   +3.28% (p=0.002 n=6)
GetTimeline_SQLite/events-1000-4           3.394m ±  1%   3.522m ±  1%   +3.77% (p=0.002 n=6)
geomean                                    223.0µ         220.5µ         -1.14%

                                   │ baseline-bench.txt │        benchmark-results.txt         │
                                   │        B/op        │     B/op      vs base                │
EventStoreAppend_InMemory-4                  785.5 ± 5%     818.0 ± 7%       ~ (p=0.394 n=6)
EventStoreAppend_SQLite-4                  1.987Ki ± 1%   1.985Ki ± 2%       ~ (p=0.727 n=6)
GetTimeline_InMemory/events-10-4           7.953Ki ± 0%   7.953Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4           46.62Ki ± 0%   46.62Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4          94.48Ki ± 0%   94.48Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4          472.8Ki ± 0%   472.8Ki ± 0%  -0.00% (p=0.032 n=6)
GetTimeline_InMemory/events-1000-4         944.3Ki ± 0%   944.3Ki ± 0%       ~ (p=0.500 n=6)
GetTimeline_SQLite/events-10-4             16.74Ki ± 0%   16.74Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4             87.14Ki ± 0%   87.14Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4            175.4Ki ± 0%   175.4Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4            846.1Ki ± 0%   846.1Ki ± 0%       ~ (p=1.000 n=6)
GetTimeline_SQLite/events-1000-4           1.639Mi ± 0%   1.639Mi ± 0%       ~ (p=1.000 n=6)
geomean                                    67.32Ki        67.54Ki       +0.33%
¹ all samples are equal

                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │     allocs/op      │  allocs/op   vs base                │
EventStoreAppend_InMemory-4                  7.000 ± 0%    7.000 ± 0%       ~ (p=1.000 n=6) ¹
EventStoreAppend_SQLite-4                    53.00 ± 0%    53.00 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-10-4             125.0 ± 0%    125.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4             653.0 ± 0%    653.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4           1.306k ± 0%   1.306k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4           6.514k ± 0%   6.514k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-1000-4          13.02k ± 0%   13.02k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-10-4               382.0 ± 0%    382.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4              1.852k ± 0%   1.852k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4             3.681k ± 0%   3.681k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4             18.54k ± 0%   18.54k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-1000-4            37.29k ± 0%   37.29k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                     1.162k        1.162k       +0.00%
¹ all samples are equal

Benchmarks run with go test -bench=. -benchmem -count=6.
Regressions ≥ 20% are flagged. Results compared via benchstat.

Important #1 — pre-scan all ghosts for protected resources before any
state mutation (infra_apply_refresh.go). The original loop could prune
an unprotected ghost then fail on a protected one, leaving partial state.
Two-pass pattern: collect all blocked names first, return error listing
every blocked resource, then execute mutations only when pre-scan passes.

Important #2 — validate --allow-protected-prune requires --refresh
(infra.go). Without this check the flag was silently no-op'd, misleading
operators. Now returns a clear pre-flight error before any work begins.

Minor #3 — replace broken docs/plans/2026-05-02-infra-drift-recovery.md
link in drift-recovery.md (design worktree path, never merged) with a
pointer to the canonical source file.

Minor #4 — markdown table was already correct standard format; no change
needed (table separator rows are standard |---|---|).

Tests added:
- TestApplyRefresh_MultipleGhostsAllOrNothing (all-or-nothing invariant)
- TestApplyRefresh_AllGhostsUnprotectedPrunesAll (pre-scan allows clean batch)
- TestInfraApply_AllowProtectedPruneRequiresRefresh (flag validation)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@intel352 intel352 merged commit 067c7d5 into main May 2, 2026
17 of 18 checks passed
@intel352 intel352 deleted the feat/apply-refresh-flag branch May 2, 2026 16:35
Copilot AI added a commit that referenced this pull request May 5, 2026
…y refresh

- cmd/wfctl/infra.go: replace inline closer.Close() block in runInfraApply's
  refresh loop with a deferred warn-on-error closure immediately after the
  provErr nil-check. Guarantees the provider connection closes regardless of
  subsequent control flow, preventing a future-maintainer refactor trap.

- cmd/wfctl/infra_apply_refresh_test.go: add strings.Contains check for
  "state mutation prune" keyword in TestApplyRefresh_AutoApprovePrunesAndApplies
  alongside the existing resource name check, so audit log format regressions
  (dropped operation keyword or misrouted log) are caught.

Relates to #519 follow-up items.

Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/d19a7660-ca79-4186-867b-f47ec64f8435

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
intel352 added a commit that referenced this pull request May 5, 2026
…refresh (#548)

* Initial plan

* fix: closer-defer pattern and audit log assertion scope in infra apply refresh

- cmd/wfctl/infra.go: replace inline closer.Close() block in runInfraApply's
  refresh loop with a deferred warn-on-error closure immediately after the
  provErr nil-check. Guarantees the provider connection closes regardless of
  subsequent control flow, preventing a future-maintainer refactor trap.

- cmd/wfctl/infra_apply_refresh_test.go: add strings.Contains check for
  "state mutation prune" keyword in TestApplyRefresh_AutoApprovePrunesAndApplies
  alongside the existing resource name check, so audit log format regressions
  (dropped operation keyword or misrouted log) are caught.

Relates to #519 follow-up items.

Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/d19a7660-ca79-4186-867b-f47ec64f8435

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>

* fix: wrap refresh provider loop body in helper so closer defers per-group

Deferring inside a for loop defers until the outer function returns, not
per-iteration. Extract the loop body into a refreshGroup helper (same
pattern as infra_plan_provider.go IIFE and infra_apply.go applyGroup),
so each provider's connection is closed as soon as its group finishes
rather than being held open for the remainder of the apply path.

Capture provType in a local variable before the defer to avoid the
loop-variable capture footgun.

Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/4b198b40-db65-4947-8a7d-c42eedd48679

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants