Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
dfa985c
feat: add LLM provider registry
ikjeong Dec 3, 2025
7b061af
refactor: extract openai provider to llm/openai
ikjeong Dec 3, 2025
000e5fe
refactor: extract claudecode provider to llm/claudecode
ikjeong Dec 3, 2025
4df773f
refactor: extract geminicli provider to llm/geminicli
ikjeong Dec 3, 2025
21a475f
refactor: extract mcp provider to llm/mcp
ikjeong Dec 3, 2025
856f23a
refactor: update client to use provider registry
ikjeong Dec 3, 2025
aa2d40a
refactor: remove legacy engine package
ikjeong Dec 3, 2025
33b3521
refactor: move llm provider imports to bootstrap
ikjeong Dec 3, 2025
dec95af
refactor: move LLMEngine interface to llm package root
ikjeong Dec 3, 2025
7f3e78e
refactor: remove unused llm/mcp provider
ikjeong Dec 4, 2025
56f3b4d
feat: add UI utilities and project config
ikjeong Dec 7, 2025
6f4c711
refactor: introduce LLM Provider interface
ikjeong Dec 7, 2025
8917bc1
refactor: migrate LLM providers to new interface
ikjeong Dec 7, 2025
f35678a
refactor: remove legacy LLM client
ikjeong Dec 7, 2025
df19e19
refactor: update adapters for Provider interface
ikjeong Dec 7, 2025
8ed8f4c
refactor: add concurrency control to converter
ikjeong Dec 7, 2025
305b516
refactor: update validators with concurrency control
ikjeong Dec 7, 2025
389c3ee
refactor: update server modules for Provider
ikjeong Dec 7, 2025
6c23784
refactor: update commands for new LLM architecture
ikjeong Dec 7, 2025
bd412ab
test: update e2e tests for new LLM architecture
ikjeong Dec 7, 2025
bc6f589
chore: update dependencies
ikjeong Dec 7, 2025
56b7d04
chore: update dependencies
ikjeong Dec 7, 2025
44dbbf5
refactor: introduce RawProvider with wrapper pattern
ikjeong Dec 7, 2025
7f66ca0
docs: update LLM README
ikjeong Dec 7, 2025
5a695bd
refactor: delegate API key handling to provider
ikjeong Dec 7, 2025
6529928
refactor: migrate settings from .env to config.json
ikjeong Dec 7, 2025
798ef0a
refactor: remove deprecated api-key command
ikjeong Dec 7, 2025
38fda04
docs: update LLM README for config.json
ikjeong Dec 7, 2025
9f62cd0
feat: add llm-validator fallback for failed rule conversions
ikjeong Dec 7, 2025
0e6b825
refactor: move provider metadata to registry pattern
ikjeong Dec 7, 2025
77eace2
refactor: remove emojis from cmd package
ikjeong Dec 8, 2025
af1a261
chore: bump version to 0.1.6 in package.json
ikjeong Dec 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,25 @@ require (
)

require (
github.com/manifoldco/promptui v0.9.0
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/modelcontextprotocol/go-sdk v1.1.0
github.com/stretchr/testify v1.11.1
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
)

require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.8 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
56 changes: 47 additions & 9 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
Expand All @@ -27,19 +36,48 @@ github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
14 changes: 12 additions & 2 deletions internal/adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,9 @@ type LinterConverter interface {
// These hints are collected and included in the LLM prompt for rule routing.
GetRoutingHints() []string

// ConvertRules converts user rules to native linter configuration using LLM
ConvertRules(ctx context.Context, rules []schema.UserRule, llmClient *llm.Client) (*LinterConfig, error)
// ConvertRules converts user rules to native linter configuration using LLM.
// Returns ConversionResult with per-rule success/failure tracking for fallback support.
ConvertRules(ctx context.Context, rules []schema.UserRule, provider llm.Provider) (*ConversionResult, error)
}

// LinterConfig represents a generated configuration file.
Expand All @@ -124,3 +125,12 @@ type LinterConfig struct {
Content []byte // File content
Format string // "json", "xml", "yaml"
}

// ConversionResult contains the conversion output with per-rule tracking.
// This allows the main converter to know which rules succeeded vs failed,
// enabling fallback to llm-validator for failed rules.
type ConversionResult struct {
Config *LinterConfig // Generated config file (may be nil if all rules failed)
SuccessRules []string // Rule IDs that converted successfully
FailedRules []string // Rule IDs that couldn't be converted (fallback to llm-validator)
}
45 changes: 31 additions & 14 deletions internal/adapter/checkstyle/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,17 @@ type checkstyleConfig struct {
Modules []checkstyleModule `xml:"module"`
}

// ConvertRules converts user rules to Checkstyle configuration using LLM
func (c *Converter) ConvertRules(ctx context.Context, rules []schema.UserRule, llmClient *llm.Client) (*adapter.LinterConfig, error) {
if llmClient == nil {
return nil, fmt.Errorf("LLM client is required")
// ConvertRules converts user rules to Checkstyle configuration using LLM.
// Returns ConversionResult with per-rule success/failure tracking for fallback support.
func (c *Converter) ConvertRules(ctx context.Context, rules []schema.UserRule, provider llm.Provider) (*adapter.ConversionResult, error) {
if provider == nil {
return nil, fmt.Errorf("LLM provider is required")
}

// Convert rules in parallel
type moduleResult struct {
index int
ruleID string
module *checkstyleModule
err error
}
Expand All @@ -85,9 +87,10 @@ func (c *Converter) ConvertRules(ctx context.Context, rules []schema.UserRule, l
go func(idx int, r schema.UserRule) {
defer wg.Done()

module, err := c.convertSingleRule(ctx, r, llmClient)
module, err := c.convertSingleRule(ctx, r, provider)
results <- moduleResult{
index: idx,
ruleID: r.ID,
module: module,
err: err,
}
Expand All @@ -99,23 +102,34 @@ func (c *Converter) ConvertRules(ctx context.Context, rules []schema.UserRule, l
close(results)
}()

// Collect modules
// Collect modules with per-rule tracking
var modules []checkstyleModule
var errors []string
successRuleIDs := make([]string, 0)
failedRuleIDs := make([]string, 0)

for result := range results {
if result.err != nil {
errors = append(errors, fmt.Sprintf("Rule %d: %v", result.index+1, result.err))
failedRuleIDs = append(failedRuleIDs, result.ruleID)
continue
}

if result.module != nil {
modules = append(modules, *result.module)
successRuleIDs = append(successRuleIDs, result.ruleID)
} else {
// Skipped = cannot be enforced by this linter
failedRuleIDs = append(failedRuleIDs, result.ruleID)
}
}

// Build result with tracking info
convResult := &adapter.ConversionResult{
SuccessRules: successRuleIDs,
FailedRules: failedRuleIDs,
}

if len(modules) == 0 {
return nil, fmt.Errorf("no rules converted: %v", errors)
return convResult, nil
}

// Separate modules into Checker-level and TreeWalker-level
Expand Down Expand Up @@ -173,15 +187,17 @@ func (c *Converter) ConvertRules(ctx context.Context, rules []schema.UserRule, l
`
fullContent := []byte(xmlHeader + string(content))

return &adapter.LinterConfig{
convResult.Config = &adapter.LinterConfig{
Filename: "checkstyle.xml",
Content: fullContent,
Format: "xml",
}, nil
}

return convResult, nil
}

// convertSingleRule converts a single rule using LLM
func (c *Converter) convertSingleRule(ctx context.Context, rule schema.UserRule, llmClient *llm.Client) (*checkstyleModule, error) {
func (c *Converter) convertSingleRule(ctx context.Context, rule schema.UserRule, provider llm.Provider) (*checkstyleModule, error) {
systemPrompt := `You are a Checkstyle configuration expert. Convert natural language Java coding rules to Checkstyle modules.

Return ONLY a JSON object (no markdown fences):
Expand Down Expand Up @@ -255,8 +271,9 @@ Output:

userPrompt := fmt.Sprintf("Convert this Java rule to Checkstyle module:\n\n%s", rule.Say)

// Call LLM with minimal complexity
response, err := llmClient.Request(systemPrompt, userPrompt).WithComplexity(llm.ComplexityMinimal).Execute(ctx)
// Call LLM
prompt := systemPrompt + "\n\n" + userPrompt
response, err := provider.Execute(ctx, prompt, llm.JSON)
if err != nil {
return nil, fmt.Errorf("LLM call failed: %w", err)
}
Expand Down
93 changes: 50 additions & 43 deletions internal/adapter/eslint/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,17 @@ func (c *Converter) GetRoutingHints() []string {
}
}

// ConvertRules converts user rules to ESLint configuration using LLM
func (c *Converter) ConvertRules(ctx context.Context, rules []schema.UserRule, llmClient *llm.Client) (*adapter.LinterConfig, error) {
if llmClient == nil {
return nil, fmt.Errorf("LLM client is required")
// ConvertRules converts user rules to ESLint configuration using LLM.
// Returns ConversionResult with per-rule success/failure tracking for fallback support.
func (c *Converter) ConvertRules(ctx context.Context, rules []schema.UserRule, provider llm.Provider) (*adapter.ConversionResult, error) {
if provider == nil {
return nil, fmt.Errorf("LLM provider is required")
}

// Convert rules in parallel using goroutines
type ruleResult struct {
index int
ruleID string
ruleName string
config interface{}
err error
Expand All @@ -68,9 +70,10 @@ func (c *Converter) ConvertRules(ctx context.Context, rules []schema.UserRule, l
go func(idx int, r schema.UserRule) {
defer wg.Done()

ruleName, config, err := c.convertSingleRule(ctx, r, llmClient)
ruleName, config, err := c.convertSingleRule(ctx, r, provider)
results <- ruleResult{
index: idx,
ruleID: r.ID,
ruleName: ruleName,
config: config,
err: err,
Expand All @@ -84,64 +87,67 @@ func (c *Converter) ConvertRules(ctx context.Context, rules []schema.UserRule, l
close(results)
}()

// Collect results
// Collect results with per-rule tracking
eslintRules := make(map[string]interface{})
var errors []string
skippedCount := 0
successRuleIDs := make([]string, 0)
failedRuleIDs := make([]string, 0)

for result := range results {
if result.err != nil {
errors = append(errors, fmt.Sprintf("Rule %d: %v", result.index+1, result.err))
fmt.Fprintf(os.Stderr, "⚠️ ESLint rule %d conversion error: %v\n", result.index+1, result.err)
failedRuleIDs = append(failedRuleIDs, result.ruleID)
fmt.Fprintf(os.Stderr, "⚠️ ESLint rule %s conversion error: %v\n", result.ruleID, result.err)
continue
}

if result.ruleName != "" {
eslintRules[result.ruleName] = result.config
fmt.Fprintf(os.Stderr, "✓ ESLint rule %d → %s\n", result.index+1, result.ruleName)
successRuleIDs = append(successRuleIDs, result.ruleID)
fmt.Fprintf(os.Stderr, "✓ ESLint rule %s → %s\n", result.ruleID, result.ruleName)
} else {
skippedCount++
fmt.Fprintf(os.Stderr, "⊘ ESLint rule %d skipped (cannot be enforced by ESLint)\n", result.index+1)
// Skipped = cannot be enforced by this linter, fallback to llm-validator
failedRuleIDs = append(failedRuleIDs, result.ruleID)
fmt.Fprintf(os.Stderr, "⊘ ESLint rule %s skipped (cannot be enforced by ESLint)\n", result.ruleID)
}
}

if skippedCount > 0 {
fmt.Fprintf(os.Stderr, "ℹ️ %d rule(s) skipped for ESLint (will use llm-validator)\n", skippedCount)
// Build result with tracking info
convResult := &adapter.ConversionResult{
SuccessRules: successRuleIDs,
FailedRules: failedRuleIDs,
}

if len(eslintRules) == 0 {
return nil, fmt.Errorf("no rules converted successfully: %v", errors)
}
// Generate config only if at least one rule succeeded
if len(eslintRules) > 0 {
eslintConfig := map[string]interface{}{
"env": map[string]bool{
"es2021": true,
"node": true,
"browser": true,
},
"parserOptions": map[string]interface{}{
"ecmaVersion": "latest",
"sourceType": "module",
},
"rules": eslintRules,
}

// Build ESLint configuration
eslintConfig := map[string]interface{}{
"env": map[string]bool{
"es2021": true,
"node": true,
"browser": true,
},
"parserOptions": map[string]interface{}{
"ecmaVersion": "latest",
"sourceType": "module",
},
"rules": eslintRules,
}
content, err := json.MarshalIndent(eslintConfig, "", " ")
if err != nil {
return nil, fmt.Errorf("failed to marshal config: %w", err)
}

// Marshal to JSON
content, err := json.MarshalIndent(eslintConfig, "", " ")
if err != nil {
return nil, fmt.Errorf("failed to marshal config: %w", err)
convResult.Config = &adapter.LinterConfig{
Filename: ".eslintrc.json",
Content: content,
Format: "json",
}
}

return &adapter.LinterConfig{
Filename: ".eslintrc.json",
Content: content,
Format: "json",
}, nil
return convResult, nil
}

// convertSingleRule converts a single user rule to ESLint rule using LLM
func (c *Converter) convertSingleRule(ctx context.Context, rule schema.UserRule, llmClient *llm.Client) (string, interface{}, error) {
func (c *Converter) convertSingleRule(ctx context.Context, rule schema.UserRule, provider llm.Provider) (string, interface{}, error) {
systemPrompt := `You are an ESLint configuration expert. Convert natural language coding rules to ESLint rule configurations.

Return ONLY a JSON object (no markdown fences) with this structure:
Expand Down Expand Up @@ -217,8 +223,9 @@ Output:
userPrompt += fmt.Sprintf("\nSeverity: %s", rule.Severity)
}

// Call LLM with minimal complexity
response, err := llmClient.Request(systemPrompt, userPrompt).WithComplexity(llm.ComplexityMinimal).Execute(ctx)
// Call LLM
prompt := systemPrompt + "\n\n" + userPrompt
response, err := provider.Execute(ctx, prompt, llm.JSON)
if err != nil {
return "", nil, fmt.Errorf("LLM call failed: %w", err)
}
Expand Down
Loading