Skip to content

Commit 28e7a88

Browse files
authored
refactor(tool): SchemaProperty minimum/maximum; migrate FuzzyFind (#321)
Extend SchemaProperty with Minimum/Maximum (interface{}, like Default, so literals survive verbatim) plus an advisory range check in validateSchema matching the existing soft-skip philosophy. Migrate FuzzyFindTool to a typed FuzzyFindInput + Schema()-backed Parameters with parity tests locking the limit bounds (1/100). Error text for malformed input changes from 'invalid input' to the standard DecodeInput message; no test asserted the old string.
1 parent f51488c commit 28e7a88

4 files changed

Lines changed: 118 additions & 23 deletions

File tree

‎internal/tool/fuzzy_find_tool.go‎

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,37 +24,49 @@ func (FuzzyFindTool) Description() string {
2424
return "Fuzzy-search a project for files by name. Handles partial names, camel-case abbreviations (CP→CachePlanner), and multi-word queries (cache gate→cache_gate.go). Returns ranked paths."
2525
}
2626

27-
func (FuzzyFindTool) Parameters() map[string]interface{} {
28-
return map[string]interface{}{
29-
"type": "object",
30-
"properties": map[string]interface{}{
31-
"query": map[string]interface{}{
32-
"type": "string",
33-
"description": "Fuzzy query: full or partial file name, camel-case abbreviation, or space-separated terms.",
27+
// FuzzyFindInput is the typed input for FuzzyFindTool.
28+
type FuzzyFindInput struct {
29+
Query string `json:"query"`
30+
Path string `json:"path"`
31+
Limit int `json:"limit"`
32+
}
33+
34+
// Schema returns the typed input schema. Parameters() delegates to it so the
35+
// two cannot diverge. The limit bounds are preserved verbatim.
36+
func (FuzzyFindTool) Schema() ToolSchema {
37+
return ToolSchema{
38+
Type: "object",
39+
Properties: map[string]SchemaProperty{
40+
"query": {
41+
Type: "string",
42+
Description: "Fuzzy query: full or partial file name, camel-case abbreviation, or space-separated terms.",
3443
},
35-
"path": map[string]interface{}{
36-
"type": "string",
37-
"description": "Project directory (default: session working directory).",
44+
"path": {
45+
Type: "string",
46+
Description: "Project directory (default: session working directory).",
3847
},
39-
"limit": map[string]interface{}{
40-
"type": "integer",
41-
"minimum": 1,
42-
"maximum": 100,
43-
"description": "Maximum results (default 20).",
48+
"limit": {
49+
Type: "integer",
50+
Minimum: 1,
51+
Maximum: 100,
52+
Description: "Maximum results (default 20).",
4453
},
4554
},
46-
"required": []string{"query"},
55+
Required: []string{"query"},
4756
}
4857
}
4958

59+
func (FuzzyFindTool) Parameters() map[string]interface{} {
60+
return fuzzyFindSchema.ToJSONSchema()
61+
}
62+
63+
// fuzzyFindSchema is the single source of truth for FuzzyFind's input schema.
64+
var fuzzyFindSchema = FuzzyFindTool{}.Schema()
65+
5066
func (FuzzyFindTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
51-
var params struct {
52-
Query string `json:"query"`
53-
Path string `json:"path"`
54-
Limit int `json:"limit"`
55-
}
56-
if err := json.Unmarshal(input, &params); err != nil {
57-
return "", fmt.Errorf("invalid input: %w", err)
67+
params, err := DecodeInput[FuzzyFindInput]("FuzzyFind", input)
68+
if err != nil {
69+
return "", err
5870
}
5971
if strings.TrimSpace(params.Query) == "" {
6072
return "", fmt.Errorf("query is required")

‎internal/tool/schema_batch_test.go‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,38 @@ func TestScreenshotSchemaProvider(t *testing.T) {
8383
t.Fatalf("required = %v, want [url]", ScreenshotTool{}.Parameters()["required"])
8484
}
8585
}
86+
87+
func TestFuzzyFindSchemaProvider(t *testing.T) {
88+
var _ SchemaProvider = FuzzyFindTool{}
89+
props := schemaProps(t, FuzzyFindTool{}.Parameters())
90+
limit, ok := props["limit"].(map[string]interface{})
91+
if !ok || limit["type"] != "integer" {
92+
t.Fatalf("limit prop = %v, want integer type", props["limit"])
93+
}
94+
// Bounds must survive the migration verbatim.
95+
if limit["minimum"] != 1 || limit["maximum"] != 100 {
96+
t.Fatalf("limit bounds = %v/%v, want 1/100", limit["minimum"], limit["maximum"])
97+
}
98+
req, _ := FuzzyFindTool{}.Parameters()["required"].([]string)
99+
if len(req) != 1 || req[0] != "query" {
100+
t.Fatalf("required = %v, want [query]", FuzzyFindTool{}.Parameters()["required"])
101+
}
102+
}
103+
104+
func TestMatchesRange(t *testing.T) {
105+
if !matchesRange("x", 1, 100) {
106+
t.Fatal("non-numeric value should pass (only numbers are ranged)")
107+
}
108+
if !matchesRange(float64(50), 1, 100) {
109+
t.Fatal("50 should be in range")
110+
}
111+
if matchesRange(float64(0), 1, 100) {
112+
t.Fatal("0 should be out of range")
113+
}
114+
if matchesRange(float64(101), 1, 100) {
115+
t.Fatal("101 should be out of range")
116+
}
117+
if !matchesRange(float64(5), nil, nil) {
118+
t.Fatal("no bounds should pass")
119+
}
120+
}

‎internal/tool/tool.go‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ type SchemaProperty struct {
5858
Enum []interface{} `json:"enum,omitempty"`
5959
// Items describes the element type for array fields.
6060
Items *SchemaProperty `json:"items,omitempty"`
61+
// Minimum/Maximum constrain numeric fields. Kept as interface{} (like
62+
// Default) so authors write plain literals and the wire output matches
63+
// hand-written schemas exactly.
64+
Minimum interface{} `json:"minimum,omitempty"`
65+
Maximum interface{} `json:"maximum,omitempty"`
6166
}
6267

6368
// SchemaProvider is an optional interface tools can implement to expose a typed
@@ -97,6 +102,12 @@ func (p SchemaProperty) toMap() map[string]interface{} {
97102
if len(p.Enum) > 0 {
98103
m["enum"] = p.Enum
99104
}
105+
if p.Minimum != nil {
106+
m["minimum"] = p.Minimum
107+
}
108+
if p.Maximum != nil {
109+
m["maximum"] = p.Maximum
110+
}
100111
if p.Items != nil {
101112
m["items"] = p.Items.toMap()
102113
}

‎internal/tool/validate_input.go‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,43 @@ func validateSchema(input map[string]interface{}, schema ToolSchema, name string
6161
if len(prop.Enum) > 0 && !matchesEnum(value, prop.Enum) {
6262
continue
6363
}
64+
if (prop.Minimum != nil || prop.Maximum != nil) && !matchesRange(value, prop.Minimum, prop.Maximum) {
65+
// Same softening as above: advisory only, caught by unit tests.
66+
continue
67+
}
68+
}
69+
}
70+
71+
func matchesRange(value, minimum, maximum interface{}) bool {
72+
n, ok := value.(float64) // JSON numbers decode as float64
73+
if !ok {
74+
return true
75+
}
76+
if minimum != nil {
77+
if min, ok := toFloat(minimum); ok && n < min {
78+
return false
79+
}
80+
}
81+
if maximum != nil {
82+
if max, ok := toFloat(maximum); ok && n > max {
83+
return false
84+
}
85+
}
86+
return true
87+
}
88+
89+
func toFloat(v interface{}) (float64, bool) {
90+
switch n := v.(type) {
91+
case float64:
92+
return n, true
93+
case float32:
94+
return float64(n), true
95+
case int:
96+
return float64(n), true
97+
case int64:
98+
return float64(n), true
99+
default:
100+
return 0, false
64101
}
65102
}
66103

0 commit comments

Comments
 (0)