Skip to content
This repository was archived by the owner on Jun 16, 2026. It is now read-only.

feat: add spec normalizer layer for declarative spec fixes#15

Merged
TommyLike merged 1 commit into
mainfrom
feat/spec-normalizer
Jun 8, 2026
Merged

feat: add spec normalizer layer for declarative spec fixes#15
TommyLike merged 1 commit into
mainfrom
feat/spec-normalizer

Conversation

@TommyLike

Copy link
Copy Markdown
Collaborator

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

Spec Load → Normalizer.Normalize() → Builder.Build()
              ↑ declarative rules per service
              │
  builtin.go registers NormalizeRule for each service

Normalizer Rules

Rule Purpose Used by
ResourceAlias Tag kebab → canonical resource name GitCode
TagReassign Path prefix → correct tag GitCode
OpIDVerbExtract Extract verb from path-encoded opId GitCode
AddMissingTags Assign tag to untagged operations Etherpad
TagRemap Direct tag rename (available)

Per-Service Configuration

  • GitCode: ResourceAlias (3) + TagReassign (7) + OpIDVerbExtract
  • Etherpad: AddMissingTags (11 untagged ops → pad)
  • GitHub/Jenkins/EUR: Clean specs, no normalization needed

Files

File Change
internal/spec/normalizer.go New: NormalizeRule + Normalizer
internal/spec/normalizer_test.go New: 6 test cases
internal/registry/builtin.go Add normalizer rules + wire into Entry
internal/registry/registry.go Entry.normalizer field + LoadSpec applies it
claude-specification/project/CLAUDE.md Spec normalizer guidelines
spec/spec-normalizer-design.md Full design document

Quality

  • go test ./... (all pass)
  • go vet ./... (clean)
  • gofmt -l . (clean)
  • ✅ Smoke: 36 passed, 0 failed, 8 skipped

Generated with Claude Code

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>
@opensourceways-bot

Copy link
Copy Markdown

Welcome To opensourceways Community

Hey @TommyLike , thanks for your contribution to the community.

Bot Usage Manual

I'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.

@opensourceways-bot

Copy link
Copy Markdown

CLA Signature Pass

TommyLike, thanks for your pull request. All authors of the commits have signed the CLA. 👍

@opensourceways-bot

Copy link
Copy Markdown

Linking Issue Notice

@TommyLike , the pull request must be linked to at least one issue.
If an issue has already been linked, but the needs-issue label remains, you can remove the label by commenting /check-issue .

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +28 to +30
// OpIDVerbExtract enables HTTP-method-prefix extraction for path-encoded
// operationIds (e.g. GitCode's get_api_v5_… → "get"/"list").
OpIDVerbExtract bool

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Comment on lines +52 to +55
// 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Comment on lines +56 to +59
// NewNormalizer creates a Normalizer for the given rules.
func NewNormalizer(rules NormalizeRule) *Normalizer {
return &Normalizer{rules: rules}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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}
}

Comment on lines +123 to +150
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
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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"}.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
// e.g. {PathPattern: "/createPad*", Tag: "pad", Method: "GET"}.
// e.g. {PathPrefix: "/createPad", Tag: "pad", Method: "GET"}.

@TommyLike TommyLike merged commit 568d6b0 into main Jun 8, 2026
7 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants