Files
rulekit/internal/cli/merge.go
NeoMody bb27f2073e Add rule optimization: domain coverage + CIDR aggregation
- domain entries covered by domain_suffix are removed
- domain/suffix entries covered by domain_keyword are removed
- child suffixes covered by parent suffix are removed
- adjacent/contained CIDRs are merged into larger blocks
- Available via --optimize/-O flag on merge and generate commands
- cn-direct: 634 -> 587 rules (-7.4%)
2026-04-01 10:19:18 +08:00

106 lines
2.4 KiB
Go

package cli
import (
"fmt"
"os"
"rulekit/internal/engine"
"rulekit/internal/model"
"text/tabwriter"
"github.com/spf13/cobra"
)
func init() {
mergeCmd := &cobra.Command{
Use: "merge <category>",
Short: "Merge rules from all sources for a category",
Args: cobra.ExactArgs(1),
RunE: runMerge,
}
mergeCmd.Flags().IntP("limit", "n", 0, "limit output rows (0 = all)")
mergeCmd.Flags().BoolP("stats", "s", false, "show statistics only")
mergeCmd.Flags().BoolP("optimize", "O", false, "optimize: merge covered domains, aggregate CIDRs")
rootCmd.AddCommand(mergeCmd)
}
func runMerge(cmd *cobra.Command, args []string) error {
categoryName := args[0]
limit, _ := cmd.Flags().GetInt("limit")
statsOnly, _ := cmd.Flags().GetBool("stats")
optimize, _ := cmd.Flags().GetBool("optimize")
cfg := loadConfig()
var merged *model.MergedRuleSet
var optResult *engine.OptimizeResult
if optimize {
var err error
merged, optResult, err = engine.MergeOptimized(cfg, categoryName)
if err != nil {
return err
}
} else {
var err error
merged, err = engine.Merge(cfg, categoryName)
if err != nil {
return err
}
}
// Stats
types := map[model.RuleType]int{}
for _, r := range merged.Rules {
types[r.Type]++
}
fmt.Printf("Merged: %s (%d rules)\n", merged.Name, len(merged.Rules))
if optResult != nil {
fmt.Printf("Optimized: %d -> %d (-%d domains by suffix, -%d by keyword, -%d CIDRs merged)\n",
optResult.Before, optResult.After,
optResult.DomainsMerged, optResult.KeywordMerged, optResult.CIDRsMerged)
}
for t, c := range types {
fmt.Printf(" %s: %d\n", t, c)
}
// Provenance summary
sourceCounts := map[string]int{}
for _, sources := range merged.Provenance {
for _, s := range sources {
sourceCounts[s]++
}
}
fmt.Printf("\nContributions:\n")
for src, count := range sourceCounts {
fmt.Printf(" %s: %d rules\n", src, count)
}
if statsOnly {
return nil
}
fmt.Println()
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintf(w, "TYPE\tVALUE\tSOURCES\n")
fmt.Fprintf(w, "----\t-----\t-------\n")
for i, r := range merged.Rules {
if limit > 0 && i >= limit {
fmt.Fprintf(w, "... (%d more)\n", len(merged.Rules)-limit)
break
}
sources := merged.Provenance[r.Key()]
srcStr := ""
for j, s := range sources {
if j > 0 {
srcStr += ", "
}
srcStr += s
}
fmt.Fprintf(w, "%s\t%s\t%s\n", r.Type, r.Value, srcStr)
}
w.Flush()
return nil
}