Verdict: NOT A BUG. The probe asserted the wrong invariant. The env-layer override is an encoding/json.Number, exactly like the operator-file layer, and that is correct. The failure is a probe defect caused by (a) using a type assertion that only matches the L0 default's Go type and (b) trusting %v / %#v to distinguish json.Number from a Go string.
Solution written to ~/solution-go-json-number-false-positive.md. Full content:
go-json-number-vs-string-type-assertion-false-positiveVerdict: NOT A BUG. The probe asserted the wrong invariant. The env-layer override is an encoding/json.Number, exactly like the operator-file layer, and that is correct. The failure is a probe defect caused by (a) using a type assertion that only matches the L0 default's Go type and (b) trusting %v / %#v to distinguish json.Number from a Go string.
src/cfgEntry.Value for engine.depth_ceiling printed as "16" under %#v and failed value.(int64); a boolean override printed as bare false.src/cfg/foreman_probe_0190_test.go → PROBE-4 FAIL: value "16" is not int64(16)| Layer | Source | Fetch/override | Integer Go type |
|---|---|---|---|
| L0 | builtin defaults | in-code literal int64(32) |
int64 |
| L1 | operator file (JSON/YAML) | decoded with json.Decoder.UseNumber() |
json.Number |
| L2 | environment (DAGGER_ENGINE_DEPTH_CEILING=16) |
decoded the same way as the file layer | json.Number |
The env layer deliberately matches the file layer so a file-supplied 16 and an env-supplied 16 are indistinguishable after merge. It does not match the L0 default's Go type. That is the intended contract.
json.Number is a defined string typetype Number string
It implements fmt.Stringer and json.Marshaler, producing misleading output:
%v → 16 (the String() method, no quotes)%#v → "16" (Go-syntax representation of the underlying string)So a print-based probe sees something that looks like a quoted Go string, while the value is a legitimate JSON number carrier. value.(int64) then fails because the dynamic type is json.Number, not int64.
A JSON false decodes to a real Go bool; %v prints false and .(bool) succeeds. The boolean branch never exercises the numeric coercion path, which is why the two overrides appeared inconsistent. There is no second defect.
The probe encoded the L0 default's dynamic type (int64) as a universal expectation. Any layer that legitimately uses a JSON number carrier reports a "wrong type" even though the semantic value is correct.
Both changes are on the probe/test side. Production code is left as-is.
// src/cfg (test helper)
import (
"encoding/json"
"fmt"
)
// asInt64 accepts every representation the config layers actually produce.
// Never assert directly to int64: L1/L2 carry json.Number.
func asInt64(v any) (int64, error) {
switch n := v.(type) {
case int64:
return n, nil
case int:
return int64(n), nil
case float64: // plain json.Unmarshal path, if ever used
return int64(n), nil
case json.Number:
return n.Int64()
default:
return 0, fmt.Errorf("unexpected numeric type %T (%#v)", v, v)
}
}
The invariant is "env representation equals file representation", not "env type equals L0 type":
func TestEnvOverride_MatchesFileLayer(t *testing.T) {
fileVal := decodeLayer(t, `16`) // L1 operator file
envVal := decodeLayer(t, `16`) // L2 environment override
l0Val := any(int64(32)) // L0 builtin default -- NOT the type contract
if _, ok := fileVal.(json.Number); !ok {
t.Fatalf("file layer type = %T, want json.Number", fileVal)
}
if _, ok := envVal.(json.Number); !ok {
t.Fatalf("env layer type = %T, want json.Number (same as file layer)", envVal)
}
got, err := asInt64(envVal)
if err != nil {
t.Fatalf("asInt64(env): %v", err)
}
if got != 16 {
t.Fatalf("engine.depth_ceiling = %d, want 16", got)
}
_ = l0Val
}
decodeLayer models the existing layers; use the real package helpers if present:
func decodeLayer(t *testing.T, s string) any {
t.Helper()
dec := json.NewDecoder(strings.NewReader(s))
dec.UseNumber() // <-- the behavior that produces json.Number
var v any
if err := dec.Decode(&v); err != nil {
t.Fatalf("decode %q: %v", s, err)
}
return v
}
json.Number.Int64() (or Float64()), or JSON round-trip them.%v / %#v to distinguish json.Number from a Go string. Use %T or a typed switch. (%T correctly prints json.Number.)If downstream code should not care which layer produced a value, normalize once at the merge boundary instead of fixing every probe:
func normalizeNumber(v any) any {
if n, ok := v.(json.Number); ok {
if i, err := n.Int64(); err == nil {
return i
}
if f, err := n.Float64(); err == nil {
return f
}
}
return v
}
This is a design choice, not a bug fix. The current layer-consistent behavior is acceptable.
file: type=json.Number %v=16 %#v="16"
env : type=json.Number %v=16 %#v="16"
l0 : type=int64 %v=32
env.(int64) ok=false <-- probe assertion (fails)
env.(json.Number) ok=true Int64=16 err=<nil> <-- correct check
bool: type=bool %v=false
This reproduces the report exactly: %#v gives "16", .(int64) is false, json.Number.Int64() yields 16, nil.
$ go test -v -run TestCorrect ./...
--- PASS: TestCorrectProbe_EnvMatchesFileLayer (0.00s)
--- PASS: TestCorrectProbe_BoolIsRealBool (0.00s)
PASS
$ go test -run TestWrongProbe ./...
panic: interface conversion: interface {} is json.Number, not int64
| Value | %T |
%v |
%#v |
Int64() |
|---|---|---|---|---|
file 16 |
json.Number |
16 |
"16" |
16, nil |
env DAGGER_ENGINE_DEPTH_CEILING=16 |
json.Number |
16 |
"16" |
16, nil |
| L0 default | int64 |
32 |
32 |
n/a |
engine.depth_ceiling (L2, env) has the same representation as the L1 file layer (json.Number), not the same Go type as the L0 default (int64).json.Number.Int64(), not value.(int64).json.Number from string with %T or a typed switch, never %v/%#v.bool (this part was always correct).foreman_probe_0190_test.go is updated to the sibling-layer comparison, and PROBE-4 passes.# Evidence - Problem class: go-json-number-vs-string-type-assertion-false-positive - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T14:52:19.556Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "An independent verification probe over the <project> src/cfg config layer asserted that an environment-layer integer override had the wrong Go type: Entry.Value for key engine.depth_ceiling (L2, source env:DAGGER_ENGINE_DEPTH_CEILING) printed as \"16\" under %#v and failed a value.(int64) type assertion, while a boolean override printed as a bare false. The probe nearly concluded a defect and triggered a rework.", "environment": "Go 1.25, <project> repo, package src/cfg", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-json-number-vs-string-type-assertion-false-positive", "provider": "openrouter", "solved_at": "2026-09-17T14:52:19.556Z", "version": "1.25.0"}Solution written to ~/solution-go-json-number-false-positive.md. Full content:
go-json-number-vs-string-type-assertion-false-positiveVerdict: NOT A BUG. The probe asserted the wrong invariant. The env-layer override is an encoding/json.Number, exactly like the operator-file layer, and that is correct. The failure is a probe defect caused by (a) using a type assertion that only matches the L0 default's Go type and (b) trusting %v / %#v to distinguish json.Number from a Go string.
src/cfgEntry.Value for engine.depth_ceiling printed as "16" under %#v and failed value.(int64); a boolean override printed as bare false.src/cfg/foreman_probe_0190_test.go → PROBE-4 FAIL: value "16" is not int64(16)| Layer | Source | Fetch/override | Integer Go type |
|---|---|---|---|
| L0 | builtin defaults | in-code literal int64(32) |
int64 |
| L1 | operator file (JSON/YAML) | decoded with json.Decoder.UseNumber() |
json.Number |
| L2 | environment (DAGGER_ENGINE_DEPTH_CEILING=16) |
decoded the same way as the file layer | json.Number |
The env layer deliberately matches the file layer so a file-supplied 16 and an env-supplied 16 are indistinguishable after merge. It does not match the L0 default's Go type. That is the intended contract.
json.Number is a defined string typetype Number string
It implements fmt.Stringer and json.Marshaler, producing misleading output:
%v → 16 (the String() method, no quotes)%#v → "16" (Go-syntax representation of the underlying string)So a print-based probe sees something that looks like a quoted Go string, while the value is a legitimate JSON number carrier. value.(int64) then fails because the dynamic type is json.Number, not int64.
A JSON false decodes to a real Go bool; %v prints false and .(bool) succeeds. The boolean branch never exercises the numeric coercion path, which is why the two overrides appeared inconsistent. There is no second defect.
The probe encoded the L0 default's dynamic type (int64) as a universal expectation. Any layer that legitimately uses a JSON number carrier reports a "wrong type" even though the semantic value is correct.
Both changes are on the probe/test side. Production code is left as-is.
// src/cfg (test helper)
import (
"encoding/json"
"fmt"
)
// asInt64 accepts every representation the config layers actually produce.
// Never assert directly to int64: L1/L2 carry json.Number.
func asInt64(v any) (int64, error) {
switch n := v.(type) {
case int64:
return n, nil
case int:
return int64(n), nil
case float64: // plain json.Unmarshal path, if ever used
return int64(n), nil
case json.Number:
return n.Int64()
default:
return 0, fmt.Errorf("unexpected numeric type %T (%#v)", v, v)
}
}
The invariant is "env representation equals file representation", not "env type equals L0 type":
func TestEnvOverride_MatchesFileLayer(t *testing.T) {
fileVal := decodeLayer(t, `16`) // L1 operator file
envVal := decodeLayer(t, `16`) // L2 environment override
l0Val := any(int64(32)) // L0 builtin default -- NOT the type contract
if _, ok := fileVal.(json.Number); !ok {
t.Fatalf("file layer type = %T, want json.Number", fileVal)
}
if _, ok := envVal.(json.Number); !ok {
t.Fatalf("env layer type = %T, want json.Number (same as file layer)", envVal)
}
got, err := asInt64(envVal)
if err != nil {
t.Fatalf("asInt64(env): %v", err)
}
if got != 16 {
t.Fatalf("engine.depth_ceiling = %d, want 16", got)
}
_ = l0Val
}
decodeLayer models the existing layers; use the real package helpers if present:
func decodeLayer(t *testing.T, s string) any {
t.Helper()
dec := json.NewDecoder(strings.NewReader(s))
dec.UseNumber() // <-- the behavior that produces json.Number
var v any
if err := dec.Decode(&v); err != nil {
t.Fatalf("decode %q: %v", s, err)
}
return v
}
json.Number.Int64() (or Float64()), or JSON round-trip them.%v / %#v to distinguish json.Number from a Go string. Use %T or a typed switch. (%T correctly prints json.Number.)If downstream code should not care which layer produced a value, normalize once at the merge boundary instead of fixing every probe:
func normalizeNumber(v any) any {
if n, ok := v.(json.Number); ok {
if i, err := n.Int64(); err == nil {
return i
}
if f, err := n.Float64(); err == nil {
return f
}
}
return v
}
This is a design choice, not a bug fix. The current layer-consistent behavior is acceptable.
file: type=json.Number %v=16 %#v="16"
env : type=json.Number %v=16 %#v="16"
l0 : type=int64 %v=32
env.(int64) ok=false <-- probe assertion (fails)
env.(json.Number) ok=true Int64=16 err=<nil> <-- correct check
bool: type=bool %v=false
This reproduces the report exactly: %#v gives "16", .(int64) is false, json.Number.Int64() yields 16, nil.
$ go test -v -run TestCorrect ./...
--- PASS: TestCorrectProbe_EnvMatchesFileLayer (0.00s)
--- PASS: TestCorrectProbe_BoolIsRealBool (0.00s)
PASS
$ go test -run TestWrongProbe ./...
panic: interface conversion: interface {} is json.Number, not int64
| Value | %T |
%v |
%#v |
Int64() |
|---|---|---|---|---|
file 16 |
json.Number |
16 |
"16" |
16, nil |
env DAGGER_ENGINE_DEPTH_CEILING=16 |
json.Number |
16 |
"16" |
16, nil |
| L0 default | int64 |
32 |
32 |
n/a |
engine.depth_ceiling (L2, env) has the same representation as the L1 file layer (json.Number), not the same Go type as the L0 default (int64).json.Number.Int64(), not value.(int64).json.Number from string with %T or a typed switch, never %v/%#v.bool (this part was always correct).foreman_probe_0190_test.go is updated to the sibling-layer comparison, and PROBE-4 passes.# Evidence - Problem class: go-json-number-vs-string-type-assertion-false-positive - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T14:52:19.556Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "An independent verification probe over the <project> src/cfg config layer asserted that an environment-layer integer override had the wrong Go type: Entry.Value for key engine.depth_ceiling (L2, source env:DAGGER_ENGINE_DEPTH_CEILING) printed as \"16\" under %#v and failed a value.(int64) type assertion, while a boolean override printed as a bare false. The probe nearly concluded a defect and triggered a rework.", "environment": "Go 1.25, <project> repo, package src/cfg", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-json-number-vs-string-type-assertion-false-positive", "provider": "openrouter", "solved_at": "2026-09-17T14:52:19.556Z", "version": "1.25.0"}