Preserve YAML string types and explicit numeric evidence
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Shared reporting and PromptKit adoption remediation
|
# Shared reporting and PromptKit adoption remediation
|
||||||
|
|
||||||
Status: four open findings; R1–R3 complete. R1–R3 are from the architectural review of
|
Status: three open findings; R1–R4 complete. R1–R3 are from the architectural review of
|
||||||
`f83866e44a8a9066db52cbd2aa5cccde0d69a839`. R4–R7 are from the algorithmic
|
`f83866e44a8a9066db52cbd2aa5cccde0d69a839`. R4–R7 are from the algorithmic
|
||||||
audit of `f5b2d1f80c4720b762f3301c29d5050710c210d0`; R4 is a correctness
|
audit of `f5b2d1f80c4720b762f3301c29d5050710c210d0`; R4 is a correctness
|
||||||
issue and R5–R7 are efficiency opportunities for consideration.
|
issue and R5–R7 are efficiency opportunities for consideration.
|
||||||
@@ -165,6 +165,13 @@ selected; their implementations below were not prescribed by the plan.
|
|||||||
|
|
||||||
## R4 — P2: YAML conversion reclassifies native strings as numbers
|
## R4 — P2: YAML conversion reclassifies native strings as numbers
|
||||||
|
|
||||||
|
**Complete.** Decoding now honors resolved string tags; core YAML emission
|
||||||
|
explicitly retains numeric tags for overflow tokens instead of relying on
|
||||||
|
heuristic decoding. String values/keys and explicit large numbers round-trip
|
||||||
|
through YAML and JSON. The independent core suite passes, and temporary native
|
||||||
|
ConfigKit consumer probes pass for whole/focused display and detailed difference
|
||||||
|
snapshots with Go 1.27.0. No optional-library dependency was added to core.
|
||||||
|
|
||||||
**Location:** [report_value.go](../../report_value.go), yamlReportValue
|
**Location:** [report_value.go](../../report_value.go), yamlReportValue
|
||||||
lines 106–118.
|
lines 106–118.
|
||||||
|
|
||||||
@@ -190,8 +197,8 @@ The shared decoder also serves focused display and difference snapshots.
|
|||||||
problem is type correctness, not its running time. Preserve native resolved
|
problem is type correctness, not its running time. Preserve native resolved
|
||||||
string tags rather than inferring numeric intent from spelling. Continue to
|
string tags rather than inferring numeric intent from spelling. Continue to
|
||||||
preserve numeric precision when numeric evidence exists, such as `!!int` or
|
preserve numeric precision when numeric evidence exists, such as `!!int` or
|
||||||
`!!float` nodes and the core's JSON number representation. Core-generated YAML
|
`!!float` nodes and the core's JSON number representation. Retain explicit numeric tags in core-generated YAML when needed; the emitter
|
||||||
already retains explicit numeric tags when needed. Do not compensate by changing
|
can otherwise omit them for overflow tokens. Do not compensate by changing
|
||||||
upstream loading or by reparsing mutable runtime configuration. Review tests
|
upstream loading or by reparsing mutable runtime configuration. Review tests
|
||||||
that currently require numeric promotion of resolved plain strings; they cannot
|
that currently require numeric promotion of resolved plain strings; they cannot
|
||||||
override the selected string-versus-number preservation contract.
|
override the selected string-versus-number preservation contract.
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ func normalizeReport(value any) (any, error) {
|
|||||||
|
|
||||||
// DecodeYAMLReport decodes one YAML document from safe inspection output into
|
// DecodeYAMLReport decodes one YAML document from safe inspection output into
|
||||||
// JSON-compatible values. Unsupported YAML meanings return an error; the result
|
// JSON-compatible values. Unsupported YAML meanings return an error; the result
|
||||||
// is not a lossless representation of the input document.
|
// is not a lossless representation of the input document. Scalar types follow
|
||||||
|
// the resolved YAML tags; numeric-looking strings remain strings.
|
||||||
func DecodeYAMLReport(reader io.Reader) (any, error) {
|
func DecodeYAMLReport(reader io.Reader) (any, error) {
|
||||||
decoder := yaml.NewDecoder(reader)
|
decoder := yaml.NewDecoder(reader)
|
||||||
var doc yaml.Node
|
var doc yaml.Node
|
||||||
@@ -104,18 +105,6 @@ func yamlReportValue(node *yaml.Node, visiting map[*yaml.Node]bool) (any, error)
|
|||||||
case yaml.ScalarNode:
|
case yaml.ScalarNode:
|
||||||
switch node.ShortTag() {
|
switch node.ShortTag() {
|
||||||
case "!!str":
|
case "!!str":
|
||||||
// The YAML resolver falls back to a string when a numeric scalar is
|
|
||||||
// outside its machine-number range. Preserve explicit and quoted
|
|
||||||
// strings, but do not silently turn a plain numeric scalar into text.
|
|
||||||
if node.Style == 0 {
|
|
||||||
plain := strings.ReplaceAll(node.Value, "_", "")
|
|
||||||
if yamlInteger.MatchString(plain) {
|
|
||||||
return yamlReportNumber(node.Value, "!!int")
|
|
||||||
}
|
|
||||||
if yamlNumericPlain.MatchString(plain) {
|
|
||||||
return yamlReportNumber(node.Value, "!!float")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return node.Value, nil
|
return node.Value, nil
|
||||||
case "!!null":
|
case "!!null":
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -188,7 +177,14 @@ func reportYAMLNode(value any) (*yaml.Node, error) {
|
|||||||
if strings.ContainsAny(string(v), ".eE") {
|
if strings.ContainsAny(string(v), ".eE") {
|
||||||
tag = "!!float"
|
tag = "!!float"
|
||||||
}
|
}
|
||||||
return &yaml.Node{Kind: yaml.ScalarNode, Tag: tag, Value: string(v)}, nil
|
node := &yaml.Node{Kind: yaml.ScalarNode, Tag: tag, Value: string(v)}
|
||||||
|
// The YAML emitter can omit numeric tags even when its resolver would
|
||||||
|
// classify an overflowing token as a string. Retain the numeric evidence
|
||||||
|
// explicitly; ParseFloat selects only the style, never the stored value.
|
||||||
|
if _, err := strconv.ParseFloat(string(v), 64); err != nil {
|
||||||
|
node.Style = yaml.TaggedStyle
|
||||||
|
}
|
||||||
|
return node, nil
|
||||||
case []any:
|
case []any:
|
||||||
node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
|
node := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"}
|
||||||
for _, item := range v {
|
for _, item := range v {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func TestNormalizeReport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestYAMLReportValues(t *testing.T) {
|
func TestYAMLReportValues(t *testing.T) {
|
||||||
input := `large: 123456789012345678901234567890
|
input := `large: !!int 123456789012345678901234567890
|
||||||
hex: 0xff
|
hex: 0xff
|
||||||
fraction: .125
|
fraction: .125
|
||||||
text: "false"
|
text: "false"
|
||||||
@@ -94,8 +94,8 @@ func TestDecodeYAMLReportRejectsUnsupportedValues(t *testing.T) {
|
|||||||
|
|
||||||
func TestYAMLReportLargeNumericScalars(t *testing.T) {
|
func TestYAMLReportLargeNumericScalars(t *testing.T) {
|
||||||
for _, tc := range []struct{ input, want string }{
|
for _, tc := range []struct{ input, want string }{
|
||||||
{"1e9999", "1e9999"}, {"0x100000000000000000000", "1208925819614629174706176"},
|
{"!!float 1e9999", "1e9999"}, {"!!int 0x100000000000000000000", "1208925819614629174706176"},
|
||||||
{"+01.2500e+9999", "1.2500e+9999"},
|
{"!!float +01.2500e+9999", "1.2500e+9999"},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.input, func(t *testing.T) {
|
t.Run(tc.input, func(t *testing.T) {
|
||||||
got, err := DecodeYAMLReport(strings.NewReader(tc.input))
|
got, err := DecodeYAMLReport(strings.NewReader(tc.input))
|
||||||
@@ -104,7 +104,7 @@ func TestYAMLReportLargeNumericScalars(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, input := range []string{`"1e9999"`, `!!str 1e9999`, `.`, `hello`} {
|
for _, input := range []string{`1e9999`, `"1e9999"`, `!!str 1e9999`, `.`, `hello`} {
|
||||||
got, err := DecodeYAMLReport(strings.NewReader(input))
|
got, err := DecodeYAMLReport(strings.NewReader(input))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -133,3 +133,37 @@ func TestReportYAMLNodePreservesNumericLookingStrings(t *testing.T) {
|
|||||||
t.Fatalf("%s: got %#v, want %#v", data, got, value)
|
t.Fatalf("%s: got %#v, want %#v", data, got, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDecodeYAMLReportPreservesPlainNativeStrings(t *testing.T) {
|
||||||
|
// Native ConfigKit safe output can contain an unquoted numeric-looking string
|
||||||
|
// when the YAML resolver considers the token outside the numeric range.
|
||||||
|
value, err := DecodeYAMLReport(strings.NewReader(`1e400: 1e400
|
||||||
|
quoted: "1e400"
|
||||||
|
explicit: !!str 1e400
|
||||||
|
number: !!float 1e400
|
||||||
|
`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := map[string]any{"1e400": "1e400", "quoted": "1e400", "explicit": "1e400", "number": json.Number("1e400")}
|
||||||
|
if !reflect.DeepEqual(value, want) {
|
||||||
|
t.Fatalf("decoded=%#v want=%#v", value, want)
|
||||||
|
}
|
||||||
|
for _, format := range []Format{FormatJSON, FormatYAML} {
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := RenderReport(&output, Report{Value: value}, format); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var got any
|
||||||
|
if format == FormatJSON {
|
||||||
|
decoder := json.NewDecoder(&output)
|
||||||
|
decoder.UseNumber()
|
||||||
|
err = decoder.Decode(&got)
|
||||||
|
} else {
|
||||||
|
got, err = DecodeYAMLReport(&output)
|
||||||
|
}
|
||||||
|
if err != nil || !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("format %v: got=%#v err=%v", format, got, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user