Skip to content

Commit 694ca4b

Browse files
authored
refactor(tool): migrate spec family to typed schemas (#336)
1 parent 22f913e commit 694ca4b

27 files changed

Lines changed: 1376 additions & 592 deletions

‎internal/tool/schema_batch_test.go‎

Lines changed: 439 additions & 0 deletions
Large diffs are not rendered by default.

‎internal/tool/spec.go‎

Lines changed: 270 additions & 137 deletions
Large diffs are not rendered by default.

‎internal/tool/spec_adaptive.go‎

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ import (
1414
type SpecAdaptiveTool struct{}
1515

1616
func (SpecAdaptiveTool) Name() string { return "SpecAdaptive" }
17+
18+
// SpecAdaptiveInput is the typed input for SpecAdaptiveTool.
19+
type SpecAdaptiveInput struct {
20+
TaskID string `json:"task_id"`
21+
EstimatedEffort int `json:"estimated_effort"`
22+
ActualEffort int `json:"actual_effort"`
23+
UnplannedDeps int `json:"unplanned_deps"`
24+
SuperScore float64 `json:"super_score"`
25+
}
26+
1727
func (SpecAdaptiveTool) Aliases() []string {
1828
return []string{"spec_adaptive", "spec:adaptive"}
1929
}
@@ -22,50 +32,39 @@ func (SpecAdaptiveTool) Description() string {
2232
return "Collect execution telemetry and compute drift score. Compares actual effort vs estimated, S.U.P.E.R compliance, and unplanned dependencies. Returns drift level (none/mild/significant/severe) and recommended corrective action."
2333
}
2434

25-
func (SpecAdaptiveTool) Parameters() map[string]interface{} {
26-
return map[string]interface{}{
27-
"type": "object",
28-
"properties": map[string]interface{}{
29-
"task_id": map[string]interface{}{
30-
"type": "string",
31-
"description": "Task identifier that was just completed",
32-
},
33-
"estimated_effort": map[string]interface{}{
34-
"type": "integer",
35-
"description": "Estimated effort in minutes",
36-
},
37-
"actual_effort": map[string]interface{}{
38-
"type": "integer",
39-
"description": "Actual effort in minutes",
40-
},
41-
"unplanned_deps": map[string]interface{}{
42-
"type": "integer",
43-
"description": "Number of unplanned dependencies encountered",
44-
},
45-
"super_score": map[string]interface{}{
46-
"type": "number",
47-
"description": "S.U.P.E.R compliance score 0.0-1.0",
48-
},
35+
// Schema returns the typed input schema. Parameters() delegates to it so the
36+
// two cannot diverge.
37+
func (SpecAdaptiveTool) Schema() ToolSchema {
38+
return ToolSchema{
39+
Type: "object",
40+
Properties: map[string]SchemaProperty{
41+
"task_id": {Type: "string", Description: "Task identifier that was just completed"},
42+
"estimated_effort": {Type: "integer", Description: "Estimated effort in minutes"},
43+
"actual_effort": {Type: "integer", Description: "Actual effort in minutes"},
44+
"unplanned_deps": {Type: "integer", Description: "Number of unplanned dependencies encountered"},
45+
"super_score": {Type: "number", Description: "S.U.P.E.R compliance score 0.0-1.0"},
4946
},
50-
"required": []string{"task_id"},
47+
48+
Required: []string{"task_id"},
5149
}
5250
}
5351

52+
func (SpecAdaptiveTool) Parameters() map[string]interface{} {
53+
return specAdaptiveSchema.ToJSONSchema()
54+
}
55+
56+
// specAdaptiveSchema is the single source of truth for SpecAdaptive's input schema.
57+
var specAdaptiveSchema = SpecAdaptiveTool{}.Schema()
58+
5459
type AdaptiveResult struct {
5560
DriftScore float64 `json:"drift_score"`
5661
DriftLevel string `json:"drift_level"`
5762
Recommendation string `json:"recommendation"`
5863
}
5964

6065
func (SpecAdaptiveTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
61-
var p struct {
62-
TaskID string `json:"task_id"`
63-
EstimatedEffort int `json:"estimated_effort"`
64-
ActualEffort int `json:"actual_effort"`
65-
UnplannedDeps int `json:"unplanned_deps"`
66-
SuperScore float64 `json:"super_score"`
67-
}
68-
if err := json.Unmarshal(input, &p); err != nil {
66+
p, err := DecodeInput[SpecAdaptiveInput]("SpecAdaptive", input)
67+
if err != nil {
6968
return "", err
7069
}
7170

‎internal/tool/spec_adr.go‎

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ import (
1212
type SpecAdrTool struct{}
1313

1414
func (SpecAdrTool) Name() string { return "SpecAdr" }
15+
16+
// SpecAdrInput is the typed input for SpecAdrTool.
17+
type SpecAdrInput struct {
18+
Action string `json:"action"`
19+
Title string `json:"title"`
20+
ReqID string `json:"req_id"`
21+
}
22+
1523
func (SpecAdrTool) Aliases() []string {
1624
return []string{"spec_adr", "spec:adr"}
1725
}
@@ -20,34 +28,29 @@ func (SpecAdrTool) Description() string {
2028
return "Create and manage Architecture Decision Records. Documents key technical decisions with context, options considered, rationale, and consequences. Links decisions to requirements they satisfy."
2129
}
2230

23-
func (SpecAdrTool) Parameters() map[string]interface{} {
24-
return map[string]interface{}{
25-
"type": "object",
26-
"properties": map[string]interface{}{
27-
"action": map[string]interface{}{
28-
"type": "string",
29-
"description": "Action: create (new ADR), list (show all), link (link to requirement)",
30-
"enum": []string{"create", "list", "link"},
31-
},
32-
"title": map[string]interface{}{
33-
"type": "string",
34-
"description": "ADR title (required for create)",
35-
},
36-
"req_id": map[string]interface{}{
37-
"type": "string",
38-
"description": "REQ ID to link to (required for link)",
39-
},
31+
// Schema returns the typed input schema. Parameters() delegates to it so the
32+
// two cannot diverge.
33+
func (SpecAdrTool) Schema() ToolSchema {
34+
return ToolSchema{
35+
Type: "object",
36+
Properties: map[string]SchemaProperty{
37+
"action": {Type: "string", Enum: []interface{}{"create", "list", "link"}, Description: "Action: create (new ADR), list (show all), link (link to requirement)"},
38+
"title": {Type: "string", Description: "ADR title (required for create)"},
39+
"req_id": {Type: "string", Description: "REQ ID to link to (required for link)"},
4040
},
4141
}
4242
}
4343

44+
func (SpecAdrTool) Parameters() map[string]interface{} {
45+
return specAdrSchema.ToJSONSchema()
46+
}
47+
48+
// specAdrSchema is the single source of truth for SpecAdr's input schema.
49+
var specAdrSchema = SpecAdrTool{}.Schema()
50+
4451
func (SpecAdrTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
45-
var p struct {
46-
Action string `json:"action"`
47-
Title string `json:"title"`
48-
ReqID string `json:"req_id"`
49-
}
50-
if err := json.Unmarshal(input, &p); err != nil {
52+
p, err := DecodeInput[SpecAdrInput]("SpecAdr", input)
53+
if err != nil {
5154
return "", err
5255
}
5356
if p.Action == "" {

‎internal/tool/spec_analyze.go‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,21 @@ func (AnalyzeTool) Description() string {
2424
return "Analyze the active spec for cross-artifact consistency: check that spec requirements are covered by plan and tasks, identify orphaned work, and report quality issues. Read-only analysis — does not modify any files."
2525
}
2626

27-
func (AnalyzeTool) Parameters() map[string]interface{} {
28-
return map[string]interface{}{
29-
"type": "object",
30-
"properties": map[string]interface{}{},
27+
// Schema returns the typed input schema. Parameters() delegates to it so the
28+
// two cannot diverge.
29+
func (AnalyzeTool) Schema() ToolSchema {
30+
return ToolSchema{
31+
Type: "object",
3132
}
3233
}
3334

35+
func (AnalyzeTool) Parameters() map[string]interface{} {
36+
return analyzeSchema.ToJSONSchema()
37+
}
38+
39+
// analyzeSchema is the single source of truth for Analyze's input schema.
40+
var analyzeSchema = AnalyzeTool{}.Schema()
41+
3442
func (AnalyzeTool) Execute(ctx context.Context, _ json.RawMessage) (string, error) {
3543
dir, err := specDir(ctx)
3644
if err != nil {

‎internal/tool/spec_bdd.go‎

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ import (
1414
type SpecBddTool struct{}
1515

1616
func (SpecBddTool) Name() string { return "SpecBdd" }
17+
18+
// SpecBddInput is the typed input for SpecBddTool.
19+
type SpecBddInput struct {
20+
Action string `json:"action"`
21+
Format string `json:"format"`
22+
}
23+
1724
func (SpecBddTool) Aliases() []string {
1825
return []string{"spec_bdd", "spec:bdd"}
1926
}
@@ -22,24 +29,25 @@ func (SpecBddTool) Description() string {
2229
return "Generate Gherkin/BDD scenarios from requirements. Converts EARS-format requirements into Given/When/Then scenarios for behavior-driven testing. Links scenarios back to requirements for traceability."
2330
}
2431

25-
func (SpecBddTool) Parameters() map[string]interface{} {
26-
return map[string]interface{}{
27-
"type": "object",
28-
"properties": map[string]interface{}{
29-
"action": map[string]interface{}{
30-
"type": "string",
31-
"description": "Action: generate (from spec), validate (check coverage), export (to feature files)",
32-
"enum": []string{"generate", "validate", "export"},
33-
},
34-
"format": map[string]interface{}{
35-
"type": "string",
36-
"description": "Output format: gherkin (default), cucumber, pytest-bdd",
37-
"enum": []string{"gherkin", "cucumber", "pytest-bdd"},
38-
},
32+
// Schema returns the typed input schema. Parameters() delegates to it so the
33+
// two cannot diverge.
34+
func (SpecBddTool) Schema() ToolSchema {
35+
return ToolSchema{
36+
Type: "object",
37+
Properties: map[string]SchemaProperty{
38+
"action": {Type: "string", Enum: []interface{}{"generate", "validate", "export"}, Description: "Action: generate (from spec), validate (check coverage), export (to feature files)"},
39+
"format": {Type: "string", Enum: []interface{}{"gherkin", "cucumber", "pytest-bdd"}, Description: "Output format: gherkin (default), cucumber, pytest-bdd"},
3940
},
4041
}
4142
}
4243

44+
func (SpecBddTool) Parameters() map[string]interface{} {
45+
return specBddSchema.ToJSONSchema()
46+
}
47+
48+
// specBddSchema is the single source of truth for SpecBdd's input schema.
49+
var specBddSchema = SpecBddTool{}.Schema()
50+
4351
type BddScenario struct {
4452
Feature string `json:"feature"`
4553
Scenario string `json:"scenario"`
@@ -48,11 +56,8 @@ type BddScenario struct {
4856
}
4957

5058
func (SpecBddTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
51-
var p struct {
52-
Action string `json:"action"`
53-
Format string `json:"format"`
54-
}
55-
if err := json.Unmarshal(input, &p); err != nil {
59+
p, err := DecodeInput[SpecBddInput]("SpecBdd", input)
60+
if err != nil {
5661
return "", err
5762
}
5863
if p.Action == "" {

‎internal/tool/spec_blast.go‎

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import (
1515
type SpecBlastTool struct{}
1616

1717
func (SpecBlastTool) Name() string { return "SpecBlast" }
18+
19+
// SpecBlastInput is the typed input for SpecBlastTool.
20+
type SpecBlastInput struct {
21+
TargetFile string `json:"target_file"`
22+
}
23+
1824
func (SpecBlastTool) Aliases() []string {
1925
return []string{"spec_blast", "spec:blast"}
2026
}
@@ -23,18 +29,24 @@ func (SpecBlastTool) Description() string {
2329
return "Blast radius analysis for proposed changes. Estimates which files, functions, and dependencies will be affected by a change before implementation."
2430
}
2531

26-
func (SpecBlastTool) Parameters() map[string]interface{} {
27-
return map[string]interface{}{
28-
"type": "object",
29-
"properties": map[string]interface{}{
30-
"target_file": map[string]interface{}{
31-
"type": "string",
32-
"description": "File to analyze for blast radius",
33-
},
32+
// Schema returns the typed input schema. Parameters() delegates to it so the
33+
// two cannot diverge.
34+
func (SpecBlastTool) Schema() ToolSchema {
35+
return ToolSchema{
36+
Type: "object",
37+
Properties: map[string]SchemaProperty{
38+
"target_file": {Type: "string", Description: "File to analyze for blast radius"},
3439
},
3540
}
3641
}
3742

43+
func (SpecBlastTool) Parameters() map[string]interface{} {
44+
return specBlastSchema.ToJSONSchema()
45+
}
46+
47+
// specBlastSchema is the single source of truth for SpecBlast's input schema.
48+
var specBlastSchema = SpecBlastTool{}.Schema()
49+
3850
type BlastResult struct {
3951
TargetFile string `json:"target_file"`
4052
DirectImpact []string `json:"direct_impact"`
@@ -45,10 +57,8 @@ type BlastResult struct {
4557
}
4658

4759
func (SpecBlastTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
48-
var p struct {
49-
TargetFile string `json:"target_file"`
50-
}
51-
if err := json.Unmarshal(input, &p); err != nil {
60+
p, err := DecodeInput[SpecBlastInput]("SpecBlast", input)
61+
if err != nil {
5262
return "", err
5363
}
5464
if p.TargetFile == "" {

‎internal/tool/spec_checklist.go‎

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ import (
1616
type ChecklistTool struct{}
1717

1818
func (ChecklistTool) Name() string { return "Checklist" }
19+
20+
// ChecklistInput is the typed input for ChecklistTool.
21+
type ChecklistInput struct {
22+
IncludeReferences bool `json:"include_references"`
23+
Artifact string `json:"artifact"`
24+
}
25+
1926
func (ChecklistTool) Aliases() []string {
2027
return []string{"checklist", "spec_checklist", "spec:checklist"}
2128
}
@@ -24,30 +31,33 @@ func (ChecklistTool) Description() string {
2431
return "Generate a QA checklist from the active spec's requirements and scenarios. Each requirement becomes a checkable item. Optionally include reference checklists for accessibility, security, performance, observability, and testing."
2532
}
2633

27-
func (ChecklistTool) Parameters() map[string]interface{} {
28-
return map[string]interface{}{
29-
"type": "object",
30-
"properties": map[string]interface{}{
31-
"include_references": map[string]interface{}{
32-
"type": "boolean",
33-
"description": "If true, append reference checklists (accessibility, security, performance, observability, testing) alongside spec-derived checks",
34-
},
35-
"artifact": map[string]interface{}{
36-
"type": "string",
37-
"description": "Which artifact to generate checklist from: spec.md (default) or tasks.md",
38-
"enum": []string{"spec.md", "tasks.md"},
39-
},
34+
// Schema returns the typed input schema. Parameters() delegates to it so the
35+
// two cannot diverge.
36+
func (ChecklistTool) Schema() ToolSchema {
37+
return ToolSchema{
38+
Type: "object",
39+
Properties: map[string]SchemaProperty{
40+
"include_references": {Type: "boolean", Description: "If true, append reference checklists (accessibility, security, performance, observability, testing) alongside spec-derived checks"},
41+
"artifact": {Type: "string", Enum: []interface{}{"spec.md", "tasks.md"}, Description: "Which artifact to generate checklist from: spec.md (default) or tasks.md"},
4042
},
4143
}
4244
}
4345

46+
func (ChecklistTool) Parameters() map[string]interface{} {
47+
return checklistSchema.ToJSONSchema()
48+
}
49+
50+
// checklistSchema is the single source of truth for Checklist's input schema.
51+
var checklistSchema = ChecklistTool{}.Schema()
52+
4453
func (ChecklistTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
45-
var p struct {
46-
IncludeReferences bool `json:"include_references"`
47-
Artifact string `json:"artifact"`
48-
}
49-
if input != nil {
50-
_ = json.Unmarshal(input, &p)
54+
var p ChecklistInput
55+
if len(input) > 0 && string(input) != "null" {
56+
decoded, err := DecodeInput[ChecklistInput]("Checklist", input)
57+
if err != nil {
58+
return "", err
59+
}
60+
p = decoded
5161
}
5262
if p.Artifact == "" {
5363
p.Artifact = "spec.md"

0 commit comments

Comments
 (0)