feat: add spec normalizer layer for declarative spec fixes#15
Conversation
New internal/spec/normalizer.go provides NormalizeRule and Normalizer that apply service-specific spec corrections before command generation. Rules are registered declaratively in builtin.go, removing the need for hardcoded if/else patches in command.go. Rules per service: - GitCode: ResourceAlias (repositories→repos, orgs→org, pull-requests→pulls) + TagReassign (repos/.../pulls→Pulls, repos/.../issues→Issues, etc.) + OpIDVerbExtract - Etherpad: AddMissingTags (12 untagged ops → pad) - GitHub/Jenkins/EUR: clean, no normalization needed Also update CLAUDE.md with spec normalizer guidelines + design doc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Welcome To opensourceways CommunityHey @TommyLike , thanks for your contribution to the community. Bot Usage ManualI'm the Bot here serving you. You can find the instructions on how to interact with me at Here . That means you can comment below every pull request or issue to trigger Bot Commands. |
CLA Signature PassTommyLike, thanks for your pull request. All authors of the commits have signed the CLA. 👍 |
Linking Issue Notice@TommyLike , the pull request must be linked to at least one issue. |
There was a problem hiding this comment.
Code Review
This pull request introduces a declarative Spec Normalizer to handle OpenAPI specification quality issues at the service registration layer, removing hardcoded patches from the command builder. It adds the NormalizeRule struct, a Normalizer utility with unit tests, and integrates them into the builtin registry. Feedback focuses on addressing dead configuration code for OpIDVerbExtract, resolving an inconsistency regarding StripVersionPrefix in the design document, optimizing the sorting of TagReassign rules during initialization rather than on every normalization pass, and correcting a minor typo in the code comments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // OpIDVerbExtract enables HTTP-method-prefix extraction for path-encoded | ||
| // operationIds (e.g. GitCode's get_api_v5_… → "get"/"list"). | ||
| OpIDVerbExtract bool |
There was a problem hiding this comment.
The OpIDVerbExtract field is defined in NormalizeRule and configured for GitCode, but it is never used or referenced anywhere in the normalizer or command builder. According to the design document, this flag is supposed to control the path-encoded operation ID verb extraction in command.go, but command.go still hardcodes this check unconditionally. This results in dead configuration code.
| // StripVersionPrefix removes leading version-like segments (api/v5) from | ||
| // path-derived names so they don't inflate priorNonParam counts. | ||
| // Applied internally by the normalizer to clean path representations. | ||
| StripVersionPrefix bool |
There was a problem hiding this comment.
The design document specifies a StripVersionPrefix rule in NormalizeRule, but this field is completely missing from the actual NormalizeRule struct implementation in internal/spec/normalizer.go. If this feature is planned for a future PR, please add a TODO comment or implement it; otherwise, remove it from the design document to avoid confusion.
| // NewNormalizer creates a Normalizer for the given rules. | ||
| func NewNormalizer(rules NormalizeRule) *Normalizer { | ||
| return &Normalizer{rules: rules} | ||
| } |
There was a problem hiding this comment.
Sorting rules.TagReassign on every call to Normalize is inefficient. Since the rules are static, we can sort the slice once in-place inside NewNormalizer during initialization. This allows applyTagReassign to use n.rules.TagReassign directly without copying and sorting on every normalization pass.
// NewNormalizer creates a Normalizer for the given rules.
func NewNormalizer(rules NormalizeRule) *Normalizer {
// Sort by path length descending so longer (more specific) prefixes
// are matched before shorter ones.
sort.Slice(rules.TagReassign, func(i, j int) bool {
return len(rules.TagReassign[i].PathPrefix) > len(rules.TagReassign[j].PathPrefix)
})
return &Normalizer{rules: rules}
}| func (n *Normalizer) applyTagReassign(spec *openapi3.T) { | ||
| if len(n.rules.TagReassign) == 0 { | ||
| return | ||
| } | ||
| // Sort by path length descending so longer (more specific) prefixes | ||
| // are matched before shorter ones. | ||
| sorted := make([]TagReassignRule, len(n.rules.TagReassign)) | ||
| copy(sorted, n.rules.TagReassign) | ||
| sort.Slice(sorted, func(i, j int) bool { | ||
| return len(sorted[i].PathPrefix) > len(sorted[j].PathPrefix) | ||
| }) | ||
|
|
||
| for path, pathItem := range spec.Paths.Map() { | ||
| if pathItem == nil { | ||
| continue | ||
| } | ||
| for _, rule := range sorted { | ||
| if strings.HasPrefix(path, rule.PathPrefix) { | ||
| for _, op := range pathItem.Operations() { | ||
| if op != nil && len(op.Tags) > 0 { | ||
| op.Tags[0] = rule.Tag | ||
| } | ||
| } | ||
| break // first (longest) match wins | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
With rules.TagReassign pre-sorted in NewNormalizer, we can remove the redundant copy and sort operations from applyTagReassign and iterate over n.rules.TagReassign directly.
func (n *Normalizer) applyTagReassign(spec *openapi3.T) {
if len(n.rules.TagReassign) == 0 {
return
}
for path, pathItem := range spec.Paths.Map() {
if pathItem == nil {
continue
}
for _, rule := range n.rules.TagReassign {
if strings.HasPrefix(path, rule.PathPrefix) {
for _, op := range pathItem.Operations() {
if op != nil && len(op.Tags) > 0 {
op.Tags[0] = rule.Tag
}
}
break // first (longest) match wins
}
}
}
}|
|
||
| // AddMissingTags assigns a tag to operations that have no tags, | ||
| // using the path to infer the appropriate tag. | ||
| // e.g. {PathPattern: "/createPad*", Tag: "pad", Method: "GET"}. |
There was a problem hiding this comment.
The example in the comment references PathPattern and a wildcard (/createPad*), but the MissingTagRule struct actually uses PathPrefix and prefix matching. Update the comment to match the actual struct fields and behavior.
| // e.g. {PathPattern: "/createPad*", Tag: "pad", Method: "GET"}. | |
| // e.g. {PathPrefix: "/createPad", Tag: "pad", Method: "GET"}. |
Summary
Add Spec Normalizer — declaration-driven spec correction layer between
spec loading and command generation. Eliminates need for hardcoded
service-specific patches in
command.go.Architecture
Normalizer Rules
ResourceAliasTagReassignOpIDVerbExtractAddMissingTagsTagRemapPer-Service Configuration
Files
internal/spec/normalizer.gointernal/spec/normalizer_test.gointernal/registry/builtin.gointernal/registry/registry.goclaude-specification/project/CLAUDE.mdspec/spec-normalizer-design.mdQuality
go test ./...(all pass)go vet ./...(clean)gofmt -l .(clean)Generated with Claude Code