Skip to content

fix(iac): configHashErr surfaces marshal failure; cache bypassed for unhashable inputs#533

Merged
intel352 merged 2 commits into
mainfrom
fix/iac-confighash-error-aware
May 4, 2026
Merged

fix(iac): configHashErr surfaces marshal failure; cache bypassed for unhashable inputs#533
intel352 merged 2 commits into
mainfrom
fix/iac-confighash-error-aware

Conversation

@intel352
Copy link
Copy Markdown
Contributor

@intel352 intel352 commented May 4, 2026

Summary

Follow-up to W-3b (PR #528) addressing Copilot's round-9 review finding, which arrived after #528 was admin-merged.

platform.configHash silently swallowed json.Marshal errors via data, _ := json.Marshal(ordered). Any input containing a non-marshalable value (channels, functions, cycles, types with broken MarshalJSON) collapsed to the constant sha256("") hash. For the diff-cache key path that meant two genuinely-different resources with such inputs would share the same SHAOutputs (or SHAConfig) and serve each other's cached DiffResult — silently misclassifying actions.

In practice IaC configs come from YAML and cannot contain those types, so this bug is unreachable for the common case. Defensive coverage matters for custom-provider Outputs that could conceivably have types with broken MarshalJSON, and for any future code path that passes non-YAML-derived configs into ComputePlan.

Fix

  1. Add configHashErr(map) (string, error) — error-aware variant that returns ("", err) on marshal failure. Backward compat: keep configHash(map) string as a wrapper that ignores the error (legacy callers operating on YAML-derived configs don't reach this path).

  2. ComputePlan + classifyModification thread a hashable flag — The candidate-bucketing loop calls configHashErr on spec.Config; on failure it sets hashable=false on the candidate. The cache-keying call site at differ.go:235 ANDs hashable into the cacheable predicate AND calls configHashErr on rs.Outputs (also bypassing on failure there). Both bypass paths fall through to unconditional driver.Diff dispatch — same defensive treatment as the empty-ProviderID path. Cost is one extra Diff call per resource with unhashable inputs, never correctness.

  3. New pins:

    • TestComputePlan_UnhashableInputs_BypassCache — two resources with channel-poisoned Outputs both re-Diff on the second ComputePlan (4 driver calls across 2 invocations); without the fix the two would collide on SHAOutputs="" and one would bogus-hit the other's cache (3 driver calls).
    • TestConfigHashErr_PropagatesMarshalFailure — unit-level assertion that configHashErr returns ("", err) on chan-input and that the un-suffixed configHash returns "" silently for the same input (backward-compat).

Source

Copilot inline comment on PR #528 round 9: #528 (comment)

Test plan

  • GOWORK=off go test -race -count=1 ./platform/... ./cmd/wfctl/... ./iac/... ./interfaces/... ./plugin/sdk/... — all green
  • TestComputePlan_UnhashableInputs_BypassCache asserts 4 driver calls (vs. bogus 3 without the fix)
  • TestConfigHashErr_PropagatesMarshalFailure pins the err-aware contract + backward-compat wrapper
  • All existing W-3b tests (TestComputePlan_EmptyProviderID_BypassesCache, TestComputePlan_NilDiffResult_CachesAsZeroValue, TestComputePlan_CacheHitSkipsDiff, etc.) still pass

🤖 Generated with Claude Code

…ypassed for unhashable inputs (Copilot review round 9)

configHash silently swallowed json.Marshal errors via `data, _ :=
json.Marshal(ordered)`, so any input containing a non-marshalable
value (channels, functions, cycles, types with broken MarshalJSON)
collapsed to the constant sha256("") hash. For the diff-cache key
path that meant two genuinely-different resources with such inputs
would share the same SHAOutputs (or SHAConfig) and serve each
other's cached DiffResult — silently misclassifying actions.

In practice IaC configs come from YAML and cannot contain those
types, so the bug is unreachable for the common case. Defensive
coverage matters for custom-provider Outputs that could conceivably
have types with broken MarshalJSON, and for any future code path
that passes non-YAML-derived configs into ComputePlan.

Fix:

1. **Add configHashErr(map) (string, error)** — error-aware variant
   that returns ("", err) on marshal failure. Backward compat: keep
   configHash(map) string as a wrapper that ignores the error
   (legacy callers operating on YAML-derived configs don't reach
   this path; documented in updated docstring).

2. **ComputePlan + classifyModification thread a `hashable` flag** —
   The candidate-bucketing loop calls configHashErr on spec.Config;
   on failure it sets `hashable=false` on the modCandidate. The cache-
   keying call site at differ.go:235 ANDs `hashable` into the
   `cacheable` predicate AND calls configHashErr on rs.Outputs (also
   bypassing on failure there). Both bypass paths fall through to
   unconditional driver.Diff dispatch — same defensive treatment as
   the empty-ProviderID path. Cost is one extra Diff call per
   resource with unhashable inputs, never correctness.

3. **New pins**:
   - TestComputePlan_UnhashableInputs_BypassCache — two resources
     with channel-poisoned Outputs both re-Diff on the second
     ComputePlan (4 driver calls across 2 invocations); without
     the fix the two would collide on SHAOutputs="" and one would
     bogus-hit the other's cache (3 driver calls).
   - TestConfigHashErr_PropagatesMarshalFailure — unit-level
     assertion that configHashErr returns ("", err) on chan-input
     and that the un-suffixed configHash returns "" silently for
     the same input (backward-compat).

Addresses Copilot inline comment on PR #528 (round 9):
- platform/differ.go:253 (configHash silently collapses unmarshalable inputs)

Tests: GOWORK=off go test -race -count=1 ./platform/... ./cmd/wfctl/...
./iac/... ./interfaces/... ./plugin/sdk/... — all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 4, 2026 07: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

This PR hardens IaC diff-cache keying in platform.ComputePlan by surfacing config/output hash failures and bypassing cache use for unhashable inputs, which avoids incorrect cache hits during provider Diff dispatch.

Changes:

  • Add an error-aware configHashErr helper and thread a hashable flag through modification classification.
  • Bypass diff-cache reads/writes when spec.Config or rs.Outputs cannot be marshaled deterministically.
  • Add regression tests covering unhashable cache inputs and the new hash helper behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
platform/differ.go Threads hashability through ComputePlan/classifyModification and adds configHashErr for cache-safe hashing.
platform/differ_cache_test.go Adds regression coverage for cache bypass on unhashable inputs and for configHashErr error propagation.

Comment thread platform/differ.go Outdated
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:254: parsing iteration count: invalid syntax
baseline-bench.txt:337638: parsing iteration count: invalid syntax
baseline-bench.txt:673717: parsing iteration count: invalid syntax
baseline-bench.txt:995666: parsing iteration count: invalid syntax
baseline-bench.txt:1315032: parsing iteration count: invalid syntax
baseline-bench.txt:1610287: parsing iteration count: invalid syntax
benchmark-results.txt:254: parsing iteration count: invalid syntax
benchmark-results.txt:324900: parsing iteration count: invalid syntax
benchmark-results.txt:626077: parsing iteration count: invalid syntax
benchmark-results.txt:918641: parsing iteration count: invalid syntax
benchmark-results.txt:1250955: parsing iteration count: invalid syntax
benchmark-results.txt:1556437: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ benchmark-results.txt │
                            │        sec/op         │
InterpreterCreation-4                 3.076m ± 217%
ComponentLoad-4                       3.545m ±  14%
ComponentExecute-4                    1.948µ ±   0%
PoolContention/workers-1-4            1.088µ ±   2%
PoolContention/workers-2-4            1.086µ ±   3%
PoolContention/workers-4-4            1.082µ ±   0%
PoolContention/workers-8-4            1.093µ ±   0%
PoolContention/workers-16-4           1.092µ ±   2%
ComponentLifecycle-4                  3.567m ±   1%
SourceValidation-4                    2.233µ ±   2%
RegistryConcurrent-4                  789.8n ±   5%
LoaderLoadFromString-4                3.577m ±   1%
geomean                               17.32µ

                            │ benchmark-results.txt │
                            │         B/op          │
InterpreterCreation-4                  2.027Mi ± 0%
ComponentLoad-4                        2.180Mi ± 0%
ComponentExecute-4                     1.203Ki ± 0%
PoolContention/workers-1-4             1.203Ki ± 0%
PoolContention/workers-2-4             1.203Ki ± 0%
PoolContention/workers-4-4             1.203Ki ± 0%
PoolContention/workers-8-4             1.203Ki ± 0%
PoolContention/workers-16-4            1.203Ki ± 0%
ComponentLifecycle-4                   2.183Mi ± 0%
SourceValidation-4                     1.984Ki ± 0%
RegistryConcurrent-4                   1.133Ki ± 0%
LoaderLoadFromString-4                 2.182Mi ± 0%
geomean                                15.25Ki

                            │ benchmark-results.txt │
                            │       allocs/op       │
InterpreterCreation-4                   15.68k ± 0%
ComponentLoad-4                         18.02k ± 0%
ComponentExecute-4                       25.00 ± 0%
PoolContention/workers-1-4               25.00 ± 0%
PoolContention/workers-2-4               25.00 ± 0%
PoolContention/workers-4-4               25.00 ± 0%
PoolContention/workers-8-4               25.00 ± 0%
PoolContention/workers-16-4              25.00 ± 0%
ComponentLifecycle-4                    18.07k ± 0%
SourceValidation-4                       32.00 ± 0%
RegistryConcurrent-4                     2.000 ± 0%
LoaderLoadFromString-4                  18.06k ± 0%
geomean                                  183.3

cpu: AMD EPYC 9V74 80-Core Processor                
                            │ baseline-bench.txt │
                            │       sec/op       │
InterpreterCreation-4              3.300m ± 196%
ComponentLoad-4                    3.529m ±   6%
ComponentExecute-4                 1.833µ ±   2%
PoolContention/workers-1-4         1.025µ ±   3%
PoolContention/workers-2-4         1.035µ ±   4%
PoolContention/workers-4-4         1.031µ ±   1%
PoolContention/workers-8-4         1.027µ ±   1%
PoolContention/workers-16-4        1.034µ ±   2%
ComponentLifecycle-4               3.545m ±   0%
SourceValidation-4                 2.066µ ±   2%
RegistryConcurrent-4               763.0n ±   2%
LoaderLoadFromString-4             3.600m ±   1%
geomean                            16.78µ

                            │ baseline-bench.txt │
                            │        B/op        │
InterpreterCreation-4               2.027Mi ± 0%
ComponentLoad-4                     2.180Mi ± 0%
ComponentExecute-4                  1.203Ki ± 0%
PoolContention/workers-1-4          1.203Ki ± 0%
PoolContention/workers-2-4          1.203Ki ± 0%
PoolContention/workers-4-4          1.203Ki ± 0%
PoolContention/workers-8-4          1.203Ki ± 0%
PoolContention/workers-16-4         1.203Ki ± 0%
ComponentLifecycle-4                2.183Mi ± 0%
SourceValidation-4                  1.984Ki ± 0%
RegistryConcurrent-4                1.133Ki ± 0%
LoaderLoadFromString-4              2.182Mi ± 0%
geomean                             15.25Ki

                            │ baseline-bench.txt │
                            │     allocs/op      │
InterpreterCreation-4                15.68k ± 0%
ComponentLoad-4                      18.02k ± 0%
ComponentExecute-4                    25.00 ± 0%
PoolContention/workers-1-4            25.00 ± 0%
PoolContention/workers-2-4            25.00 ± 0%
PoolContention/workers-4-4            25.00 ± 0%
PoolContention/workers-8-4            25.00 ± 0%
PoolContention/workers-16-4           25.00 ± 0%
ComponentLifecycle-4                 18.07k ± 0%
SourceValidation-4                    32.00 ± 0%
RegistryConcurrent-4                  2.000 ± 0%
LoaderLoadFromString-4               18.06k ± 0%
geomean                               183.3

pkg: github.com/GoCodeAlone/workflow/middleware
cpu: AMD EPYC 7763 64-Core Processor                
                                  │ benchmark-results.txt │
                                  │        sec/op         │
CircuitBreakerDetection-4                     282.6n ± 8%
CircuitBreakerExecution_Success-4             21.54n ± 0%
CircuitBreakerExecution_Failure-4             66.26n ± 0%
geomean                                       73.88n

                                  │ benchmark-results.txt │
                                  │         B/op          │
CircuitBreakerDetection-4                    144.0 ± 0%
CircuitBreakerExecution_Success-4            0.000 ± 0%
CircuitBreakerExecution_Failure-4            0.000 ± 0%
geomean                                                 ¹
¹ summaries must be >0 to compute geomean

                                  │ benchmark-results.txt │
                                  │       allocs/op       │
CircuitBreakerDetection-4                    1.000 ± 0%
CircuitBreakerExecution_Success-4            0.000 ± 0%
CircuitBreakerExecution_Failure-4            0.000 ± 0%
geomean                                                 ¹
¹ summaries must be >0 to compute geomean

cpu: AMD EPYC 9V74 80-Core Processor                
                                  │ baseline-bench.txt │
                                  │       sec/op       │
CircuitBreakerDetection-4                  300.0n ± 6%
CircuitBreakerExecution_Success-4          22.68n ± 0%
CircuitBreakerExecution_Failure-4          70.94n ± 0%
geomean                                    78.45n

                                  │ baseline-bench.txt │
                                  │        B/op        │
CircuitBreakerDetection-4                 144.0 ± 0%
CircuitBreakerExecution_Success-4         0.000 ± 0%
CircuitBreakerExecution_Failure-4         0.000 ± 0%
geomean                                              ¹
¹ summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │
                                  │     allocs/op      │
CircuitBreakerDetection-4                 1.000 ± 0%
CircuitBreakerExecution_Success-4         0.000 ± 0%
CircuitBreakerExecution_Failure-4         0.000 ± 0%
geomean                                              ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
cpu: AMD EPYC 7763 64-Core Processor                
                                 │ benchmark-results.txt │
                                 │        sec/op         │
JQTransform_Simple-4                        885.1n ± 29%
JQTransform_ObjectConstruction-4            1.464µ ±  0%
JQTransform_ArraySelect-4                   3.344µ ±  1%
JQTransform_Complex-4                       37.88µ ±  0%
JQTransform_Throughput-4                    1.793µ ±  0%
SSEPublishDelivery-4                        76.83n ±  1%
geomean                                     1.681µ

                                 │ benchmark-results.txt │
                                 │         B/op          │
JQTransform_Simple-4                      1.273Ki ± 0%
JQTransform_ObjectConstruction-4          1.773Ki ± 0%
JQTransform_ArraySelect-4                 2.625Ki ± 0%
JQTransform_Complex-4                     16.22Ki ± 0%
JQTransform_Throughput-4                  1.984Ki ± 0%
SSEPublishDelivery-4                        0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

                                 │ benchmark-results.txt │
                                 │       allocs/op       │
JQTransform_Simple-4                        10.00 ± 0%
JQTransform_ObjectConstruction-4            15.00 ± 0%
JQTransform_ArraySelect-4                   30.00 ± 0%
JQTransform_Complex-4                       324.0 ± 0%
JQTransform_Throughput-4                    17.00 ± 0%
SSEPublishDelivery-4                        0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

cpu: AMD EPYC 9V74 80-Core Processor                
                                 │ baseline-bench.txt │
                                 │       sec/op       │
JQTransform_Simple-4                     811.4n ± 31%
JQTransform_ObjectConstruction-4         1.395µ ±  1%
JQTransform_ArraySelect-4                3.355µ ±  1%
JQTransform_Complex-4                    41.65µ ±  2%
JQTransform_Throughput-4                 1.712µ ±  1%
SSEPublishDelivery-4                     64.84n ±  2%
geomean                                  1.612µ

                                 │ baseline-bench.txt │
                                 │        B/op        │
JQTransform_Simple-4                   1.273Ki ± 0%
JQTransform_ObjectConstruction-4       1.773Ki ± 0%
JQTransform_ArraySelect-4              2.625Ki ± 0%
JQTransform_Complex-4                  16.22Ki ± 0%
JQTransform_Throughput-4               1.984Ki ± 0%
SSEPublishDelivery-4                     0.000 ± 0%
geomean                                             ¹
¹ summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │
                                 │     allocs/op      │
JQTransform_Simple-4                     10.00 ± 0%
JQTransform_ObjectConstruction-4         15.00 ± 0%
JQTransform_ArraySelect-4                30.00 ± 0%
JQTransform_Complex-4                    324.0 ± 0%
JQTransform_Throughput-4                 17.00 ± 0%
SSEPublishDelivery-4                     0.000 ± 0%
geomean                                             ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
cpu: AMD EPYC 7763 64-Core Processor                
                                    │ benchmark-results.txt │
                                    │        sec/op         │
SchemaValidation_Simple-4                      1.097µ ± 14%
SchemaValidation_AllFields-4                   1.652µ ±  1%
SchemaValidation_FormatValidation-4            1.584µ ±  1%
SchemaValidation_ManySchemas-4                 1.843µ ±  5%
geomean                                        1.517µ

                                    │ benchmark-results.txt │
                                    │         B/op          │
SchemaValidation_Simple-4                      0.000 ± 0%
SchemaValidation_AllFields-4                   0.000 ± 0%
SchemaValidation_FormatValidation-4            0.000 ± 0%
SchemaValidation_ManySchemas-4                 0.000 ± 0%
geomean                                                   ¹
¹ summaries must be >0 to compute geomean

                                    │ benchmark-results.txt │
                                    │       allocs/op       │
SchemaValidation_Simple-4                      0.000 ± 0%
SchemaValidation_AllFields-4                   0.000 ± 0%
SchemaValidation_FormatValidation-4            0.000 ± 0%
SchemaValidation_ManySchemas-4                 0.000 ± 0%
geomean                                                   ¹
¹ summaries must be >0 to compute geomean

cpu: AMD EPYC 9V74 80-Core Processor                
                                    │ baseline-bench.txt │
                                    │       sec/op       │
SchemaValidation_Simple-4                   1.126µ ± 17%
SchemaValidation_AllFields-4                1.650µ ±  2%
SchemaValidation_FormatValidation-4         1.589µ ±  1%
SchemaValidation_ManySchemas-4              1.616µ ±  2%
geomean                                     1.478µ

                                    │ baseline-bench.txt │
                                    │        B/op        │
SchemaValidation_Simple-4                   0.000 ± 0%
SchemaValidation_AllFields-4                0.000 ± 0%
SchemaValidation_FormatValidation-4         0.000 ± 0%
SchemaValidation_ManySchemas-4              0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │
                                    │     allocs/op      │
SchemaValidation_Simple-4                   0.000 ± 0%
SchemaValidation_AllFields-4                0.000 ± 0%
SchemaValidation_FormatValidation-4         0.000 ± 0%
SchemaValidation_ManySchemas-4              0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
cpu: AMD EPYC 7763 64-Core Processor                
                                   │ benchmark-results.txt │
                                   │        sec/op         │
EventStoreAppend_InMemory-4                   1.160µ ± 32%
EventStoreAppend_SQLite-4                     1.333m ±  6%
GetTimeline_InMemory/events-10-4              13.67µ ±  4%
GetTimeline_InMemory/events-50-4              76.23µ ± 21%
GetTimeline_InMemory/events-100-4             120.2µ ±  0%
GetTimeline_InMemory/events-500-4             620.6µ ±  1%
GetTimeline_InMemory/events-1000-4            1.267m ±  1%
GetTimeline_SQLite/events-10-4                105.9µ ±  0%
GetTimeline_SQLite/events-50-4                244.2µ ±  2%
GetTimeline_SQLite/events-100-4               412.6µ ±  0%
GetTimeline_SQLite/events-500-4               1.761m ±  0%
GetTimeline_SQLite/events-1000-4              3.436m ±  0%
geomean                                       215.1µ

                                   │ benchmark-results.txt │
                                   │         B/op          │
EventStoreAppend_InMemory-4                     792.0 ± 8%
EventStoreAppend_SQLite-4                     1.984Ki ± 2%
GetTimeline_InMemory/events-10-4              7.953Ki ± 0%
GetTimeline_InMemory/events-50-4              46.62Ki ± 0%
GetTimeline_InMemory/events-100-4             94.48Ki ± 0%
GetTimeline_InMemory/events-500-4             472.8Ki ± 0%
GetTimeline_InMemory/events-1000-4            944.3Ki ± 0%
GetTimeline_SQLite/events-10-4                16.74Ki ± 0%
GetTimeline_SQLite/events-50-4                87.14Ki ± 0%
GetTimeline_SQLite/events-100-4               175.4Ki ± 0%
GetTimeline_SQLite/events-500-4               846.1Ki ± 0%
GetTimeline_SQLite/events-1000-4              1.639Mi ± 0%
geomean                                       67.36Ki

                                   │ benchmark-results.txt │
                                   │       allocs/op       │
EventStoreAppend_InMemory-4                     7.000 ± 0%
EventStoreAppend_SQLite-4                       53.00 ± 0%
GetTimeline_InMemory/events-10-4                125.0 ± 0%
GetTimeline_InMemory/events-50-4                653.0 ± 0%
GetTimeline_InMemory/events-100-4              1.306k ± 0%
GetTimeline_InMemory/events-500-4              6.514k ± 0%
GetTimeline_InMemory/events-1000-4             13.02k ± 0%
GetTimeline_SQLite/events-10-4                  382.0 ± 0%
GetTimeline_SQLite/events-50-4                 1.852k ± 0%
GetTimeline_SQLite/events-100-4                3.681k ± 0%
GetTimeline_SQLite/events-500-4                18.54k ± 0%
GetTimeline_SQLite/events-1000-4               37.29k ± 0%
geomean                                        1.162k

cpu: AMD EPYC 9V74 80-Core Processor                
                                   │ baseline-bench.txt │
                                   │       sec/op       │
EventStoreAppend_InMemory-4                1.062µ ± 10%
EventStoreAppend_SQLite-4                  1.053m ±  4%
GetTimeline_InMemory/events-10-4           12.89µ ±  4%
GetTimeline_InMemory/events-50-4           65.18µ ± 18%
GetTimeline_InMemory/events-100-4          106.9µ ±  1%
GetTimeline_InMemory/events-500-4          544.8µ ±  2%
GetTimeline_InMemory/events-1000-4         1.107m ±  3%
GetTimeline_SQLite/events-10-4             82.55µ ±  1%
GetTimeline_SQLite/events-50-4             218.2µ ±  1%
GetTimeline_SQLite/events-100-4            384.3µ ±  0%
GetTimeline_SQLite/events-500-4            1.668m ±  0%
GetTimeline_SQLite/events-1000-4           3.251m ±  1%
geomean                                    190.4µ

                                   │ baseline-bench.txt │
                                   │        B/op        │
EventStoreAppend_InMemory-4                  781.5 ± 5%
EventStoreAppend_SQLite-4                  1.983Ki ± 1%
GetTimeline_InMemory/events-10-4           7.953Ki ± 0%
GetTimeline_InMemory/events-50-4           46.62Ki ± 0%
GetTimeline_InMemory/events-100-4          94.48Ki ± 0%
GetTimeline_InMemory/events-500-4          472.8Ki ± 0%
GetTimeline_InMemory/events-1000-4         944.3Ki ± 0%
GetTimeline_SQLite/events-10-4             16.74Ki ± 0%
GetTimeline_SQLite/events-50-4             87.14Ki ± 0%
GetTimeline_SQLite/events-100-4            175.4Ki ± 0%
GetTimeline_SQLite/events-500-4            846.1Ki ± 0%
GetTimeline_SQLite/events-1000-4           1.639Mi ± 0%
geomean                                    67.28Ki

                                   │ baseline-bench.txt │
                                   │     allocs/op      │
EventStoreAppend_InMemory-4                  7.000 ± 0%
EventStoreAppend_SQLite-4                    53.00 ± 0%
GetTimeline_InMemory/events-10-4             125.0 ± 0%
GetTimeline_InMemory/events-50-4             653.0 ± 0%
GetTimeline_InMemory/events-100-4           1.306k ± 0%
GetTimeline_InMemory/events-500-4           6.514k ± 0%
GetTimeline_InMemory/events-1000-4          13.02k ± 0%
GetTimeline_SQLite/events-10-4               382.0 ± 0%
GetTimeline_SQLite/events-50-4              1.852k ± 0%
GetTimeline_SQLite/events-100-4             3.681k ± 0%
GetTimeline_SQLite/events-500-4             18.54k ± 0%
GetTimeline_SQLite/events-1000-4            37.29k ± 0%
geomean                                     1.162k

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

…al failure for backward-compat (Copilot review)

Copilot R9 caught a silent ABI break in T3.6: the new configHash
wrapper returned "" for unmarshalable inputs, whereas the pre-T3.6
implementation returned sha256(nil) = e3b0c44... (the well-known
sha256 of empty bytes; data is nil after json.Marshal failure).

That changes any persisted ResolvedConfigHash / ConfigHash values
computed for an unhashable input — they would no longer compare
equal across the upgrade boundary. While unmarshalable values are
unreachable for YAML-derived configs in practice, the public
platform.ConfigHash function is part of the API surface, so the
defensive choice is byte-for-byte stability.

Restored configHash to the pre-T3.6 sha256-of-best-effort body.
Cache-key callers continue to use configHashErr (the strict variant
that surfaces the marshal error so the cache is bypassed for that
resource — collapsing all unmarshalable inputs to a constant hash
would risk cache-key collisions across distinct resources).

ComputePlan now falls back to configHash for the stored
ResolvedConfigHash when configHashErr fails, while still using the
hashable flag to gate cache participation in classifyModification.

Test TestConfigHashErr_PropagatesMarshalFailure pinned to the
restored legacy contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@intel352 intel352 merged commit 8f1b907 into main May 4, 2026
17 of 18 checks passed
@intel352 intel352 deleted the fix/iac-confighash-error-aware branch May 4, 2026 07:59
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