Merge branch 'feat/ml-integration' into dev
# Conflicts: # .gh-actions/ISSUE_TEMPLATE/config.yml # .gh-actions/workflows/alpha-release-manual.yml # .gh-actions/workflows/alpha-release-push.yml # .gh-actions/workflows/alpha-release.yml # .gh-actions/workflows/bugseti-release.yml # .gh-actions/workflows/ci-manual.yml # .gh-actions/workflows/ci-pull-request.yml # .gh-actions/workflows/ci-push.yml # .gh-actions/workflows/ci.yml # .gh-actions/workflows/coverage-manual.yml # .gh-actions/workflows/coverage-pull-request.yml # .gh-actions/workflows/coverage-push.yml # .gh-actions/workflows/coverage.yml # .gh-actions/workflows/release.yml # cmd/bugseti/go.mod # cmd/bugseti/workspace.go # go.sum # internal/bugseti/submit.go # internal/bugseti/updater/go.mod # internal/cmd/ml/cmd_ml.go # internal/core-ide/go.mod # internal/variants/full.go # pkg/ml/db.go
This commit is contained in:
commit
48d385279b
10 changed files with 1257 additions and 1706 deletions
|
|
@ -7,6 +7,9 @@ require (
|
|||
forge.lthn.ai/core/cli/internal/bugseti v0.0.0
|
||||
forge.lthn.ai/core/cli/internal/bugseti/updater v0.0.0
|
||||
github.com/Snider/Borg v0.2.0
|
||||
forge.lthn.ai/core/cli v0.0.0
|
||||
forge.lthn.ai/core/cli/internal/bugseti v0.0.0
|
||||
forge.lthn.ai/core/cli/internal/bugseti/updater v0.0.0
|
||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.64
|
||||
)
|
||||
|
||||
|
|
|
|||
BIN
core-ide
Executable file
BIN
core-ide
Executable file
Binary file not shown.
|
|
@ -313,7 +313,7 @@ func (s *SubmitService) generatePRBody(issue *Issue) string {
|
|||
body.WriteString("<!-- Describe how you tested your changes -->\n\n")
|
||||
|
||||
body.WriteString("---\n\n")
|
||||
body.WriteString("*Submitted via [BugSETI](https://bugseti.app) - Distributed Bug Fixing*\n")
|
||||
body.WriteString("*Submitted via [BugSETI](https://forge.lthn.ai/core/cli) - Distributed Bug Fixing*\n")
|
||||
|
||||
return body.String()
|
||||
}
|
||||
|
|
|
|||
11
internal/core-ide/frontend/package-lock.json
generated
11
internal/core-ide/frontend/package-lock.json
generated
|
|
@ -5495,17 +5495,6 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.11.7",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
|
||||
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hosted-git-info": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ require (
|
|||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
forge.lthn.ai/core/cli v0.0.0
|
||||
forge.lthn.ai/core/cli-gui v0.0.0
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
|
|
|
|||
560
pkg/devkit/devkit.go
Normal file
560
pkg/devkit/devkit.go
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
// Package devkit provides a developer toolkit for common automation commands.
|
||||
// Designed by Gemini 3 Pro (Hypnos) + Claude Opus (Charon), signed LEK-1 | lthn.ai | EUPL-1.2
|
||||
package devkit
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// --- Code Quality ---
|
||||
|
||||
// Finding represents a single issue found by a linting tool.
|
||||
type Finding struct {
|
||||
File string
|
||||
Line int
|
||||
Message string
|
||||
Tool string
|
||||
}
|
||||
|
||||
// CoverageReport holds the test coverage percentage for a package.
|
||||
type CoverageReport struct {
|
||||
Package string
|
||||
Percentage float64
|
||||
}
|
||||
|
||||
// RaceCondition represents a data race detected by the Go race detector.
|
||||
type RaceCondition struct {
|
||||
File string
|
||||
Line int
|
||||
Desc string
|
||||
}
|
||||
|
||||
// TODO represents a tracked code comment like TODO, FIXME, or HACK.
|
||||
type TODO struct {
|
||||
File string
|
||||
Line int
|
||||
Type string
|
||||
Message string
|
||||
}
|
||||
|
||||
// --- Security ---
|
||||
|
||||
// Vulnerability represents a dependency vulnerability.
|
||||
type Vulnerability struct {
|
||||
ID string
|
||||
Package string
|
||||
Version string
|
||||
Description string
|
||||
}
|
||||
|
||||
// SecretLeak represents a potential secret found in the codebase.
|
||||
type SecretLeak struct {
|
||||
File string
|
||||
Line int
|
||||
RuleID string
|
||||
Match string
|
||||
}
|
||||
|
||||
// PermIssue represents a file permission issue.
|
||||
type PermIssue struct {
|
||||
File string
|
||||
Permission string
|
||||
Issue string
|
||||
}
|
||||
|
||||
// --- Git Operations ---
|
||||
|
||||
// DiffSummary provides a summary of changes.
|
||||
type DiffSummary struct {
|
||||
FilesChanged int
|
||||
Insertions int
|
||||
Deletions int
|
||||
}
|
||||
|
||||
// Commit represents a single git commit.
|
||||
type Commit struct {
|
||||
Hash string
|
||||
Author string
|
||||
Date time.Time
|
||||
Message string
|
||||
}
|
||||
|
||||
// --- Build & Dependencies ---
|
||||
|
||||
// BuildResult holds the outcome of a single build target.
|
||||
type BuildResult struct {
|
||||
Target string
|
||||
Path string
|
||||
Error error
|
||||
}
|
||||
|
||||
// Graph represents a dependency graph.
|
||||
type Graph struct {
|
||||
Nodes []string
|
||||
Edges map[string][]string
|
||||
}
|
||||
|
||||
// --- Metrics ---
|
||||
|
||||
// ComplexFunc represents a function with its cyclomatic complexity score.
|
||||
type ComplexFunc struct {
|
||||
Package string
|
||||
FuncName string
|
||||
File string
|
||||
Line int
|
||||
Score int
|
||||
}
|
||||
|
||||
// Toolkit wraps common dev automation commands into structured Go APIs.
|
||||
type Toolkit struct {
|
||||
Dir string // Working directory for commands
|
||||
}
|
||||
|
||||
// New creates a Toolkit rooted at the given directory.
|
||||
func New(dir string) *Toolkit {
|
||||
return &Toolkit{Dir: dir}
|
||||
}
|
||||
|
||||
// Run executes a command and captures stdout, stderr, and exit code.
|
||||
func (t *Toolkit) Run(name string, args ...string) (stdout, stderr string, exitCode int, err error) {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.Dir = t.Dir
|
||||
var stdoutBuf, stderrBuf bytes.Buffer
|
||||
cmd.Stdout = &stdoutBuf
|
||||
cmd.Stderr = &stderrBuf
|
||||
|
||||
err = cmd.Run()
|
||||
stdout = stdoutBuf.String()
|
||||
stderr = stderrBuf.String()
|
||||
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
exitCode = exitErr.ExitCode()
|
||||
} else {
|
||||
exitCode = -1
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FindTODOs greps for TODO/FIXME/HACK comments within a directory.
|
||||
func (t *Toolkit) FindTODOs(dir string) ([]TODO, error) {
|
||||
pattern := `\b(TODO|FIXME|HACK)\b(\(.*\))?:`
|
||||
stdout, stderr, exitCode, err := t.Run("git", "grep", "--line-number", "-E", pattern, "--", dir)
|
||||
|
||||
if exitCode == 1 && stdout == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil && exitCode != 1 {
|
||||
return nil, fmt.Errorf("git grep failed (exit %d): %s\n%s", exitCode, err, stderr)
|
||||
}
|
||||
|
||||
var todos []TODO
|
||||
re := regexp.MustCompile(pattern)
|
||||
|
||||
for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, ":", 3)
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
lineNum, _ := strconv.Atoi(parts[1])
|
||||
match := re.FindStringSubmatch(parts[2])
|
||||
todoType := ""
|
||||
if len(match) > 1 {
|
||||
todoType = match[1]
|
||||
}
|
||||
msg := strings.TrimSpace(re.Split(parts[2], 2)[1])
|
||||
|
||||
todos = append(todos, TODO{
|
||||
File: parts[0],
|
||||
Line: lineNum,
|
||||
Type: todoType,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
return todos, nil
|
||||
}
|
||||
|
||||
// AuditDeps runs govulncheck to find dependency vulnerabilities.
|
||||
func (t *Toolkit) AuditDeps() ([]Vulnerability, error) {
|
||||
stdout, stderr, exitCode, err := t.Run("govulncheck", "./...")
|
||||
if err != nil && exitCode != 0 && !strings.Contains(stdout, "Vulnerability") {
|
||||
return nil, fmt.Errorf("govulncheck failed (exit %d): %s\n%s", exitCode, err, stderr)
|
||||
}
|
||||
|
||||
var vulns []Vulnerability
|
||||
scanner := bufio.NewScanner(strings.NewReader(stdout))
|
||||
var cur Vulnerability
|
||||
inBlock := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "Vulnerability #") {
|
||||
if cur.ID != "" {
|
||||
vulns = append(vulns, cur)
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
cur = Vulnerability{}
|
||||
if len(fields) > 1 {
|
||||
cur.ID = fields[1]
|
||||
}
|
||||
inBlock = true
|
||||
} else if inBlock {
|
||||
switch {
|
||||
case strings.Contains(line, "Package:"):
|
||||
cur.Package = strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
|
||||
case strings.Contains(line, "Found in version:"):
|
||||
cur.Version = strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
|
||||
case line == "":
|
||||
if cur.ID != "" {
|
||||
vulns = append(vulns, cur)
|
||||
cur = Vulnerability{}
|
||||
}
|
||||
inBlock = false
|
||||
default:
|
||||
if !strings.HasPrefix(line, " ") && cur.Description == "" {
|
||||
cur.Description = strings.TrimSpace(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cur.ID != "" {
|
||||
vulns = append(vulns, cur)
|
||||
}
|
||||
return vulns, nil
|
||||
}
|
||||
|
||||
// DiffStat returns a summary of uncommitted changes.
|
||||
func (t *Toolkit) DiffStat() (DiffSummary, error) {
|
||||
stdout, stderr, exitCode, err := t.Run("git", "diff", "--stat")
|
||||
if err != nil && exitCode != 0 {
|
||||
return DiffSummary{}, fmt.Errorf("git diff failed (exit %d): %s\n%s", exitCode, err, stderr)
|
||||
}
|
||||
|
||||
var s DiffSummary
|
||||
lines := strings.Split(strings.TrimSpace(stdout), "\n")
|
||||
if len(lines) == 0 || lines[0] == "" {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
last := lines[len(lines)-1]
|
||||
for _, part := range strings.Split(last, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
fields := strings.Fields(part)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
val, _ := strconv.Atoi(fields[0])
|
||||
switch {
|
||||
case strings.Contains(part, "file"):
|
||||
s.FilesChanged = val
|
||||
case strings.Contains(part, "insertion"):
|
||||
s.Insertions = val
|
||||
case strings.Contains(part, "deletion"):
|
||||
s.Deletions = val
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// UncommittedFiles returns paths of files with uncommitted changes.
|
||||
func (t *Toolkit) UncommittedFiles() ([]string, error) {
|
||||
stdout, stderr, exitCode, err := t.Run("git", "status", "--porcelain")
|
||||
if err != nil && exitCode != 0 {
|
||||
return nil, fmt.Errorf("git status failed: %s\n%s", err, stderr)
|
||||
}
|
||||
var files []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") {
|
||||
if len(line) > 3 {
|
||||
files = append(files, strings.TrimSpace(line[3:]))
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// Lint runs go vet on the given package pattern.
|
||||
func (t *Toolkit) Lint(pkg string) ([]Finding, error) {
|
||||
_, stderr, exitCode, err := t.Run("go", "vet", pkg)
|
||||
if exitCode == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil && exitCode != 2 {
|
||||
return nil, fmt.Errorf("go vet failed: %w", err)
|
||||
}
|
||||
|
||||
var findings []Finding
|
||||
for _, line := range strings.Split(strings.TrimSpace(stderr), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, ":", 4)
|
||||
if len(parts) < 4 {
|
||||
continue
|
||||
}
|
||||
lineNum, _ := strconv.Atoi(parts[1])
|
||||
findings = append(findings, Finding{
|
||||
File: parts[0],
|
||||
Line: lineNum,
|
||||
Message: strings.TrimSpace(parts[3]),
|
||||
Tool: "go vet",
|
||||
})
|
||||
}
|
||||
return findings, nil
|
||||
}
|
||||
|
||||
// ScanSecrets runs gitleaks to find potential secret leaks.
|
||||
func (t *Toolkit) ScanSecrets(dir string) ([]SecretLeak, error) {
|
||||
stdout, _, exitCode, err := t.Run("gitleaks", "detect", "--source", dir, "--report-format", "csv", "--no-git")
|
||||
if exitCode == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil && exitCode != 1 {
|
||||
return nil, fmt.Errorf("gitleaks failed: %w", err)
|
||||
}
|
||||
|
||||
var leaks []SecretLeak
|
||||
for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") {
|
||||
if line == "" || strings.HasPrefix(line, "RuleID") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, ",", 4)
|
||||
if len(parts) < 4 {
|
||||
continue
|
||||
}
|
||||
lineNum, _ := strconv.Atoi(parts[2])
|
||||
leaks = append(leaks, SecretLeak{
|
||||
RuleID: parts[0],
|
||||
File: parts[1],
|
||||
Line: lineNum,
|
||||
Match: parts[3],
|
||||
})
|
||||
}
|
||||
return leaks, nil
|
||||
}
|
||||
|
||||
// ModTidy runs go mod tidy.
|
||||
func (t *Toolkit) ModTidy() error {
|
||||
_, stderr, exitCode, err := t.Run("go", "mod", "tidy")
|
||||
if err != nil && exitCode != 0 {
|
||||
return fmt.Errorf("go mod tidy failed: %s", stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build compiles the given targets.
|
||||
func (t *Toolkit) Build(targets ...string) ([]BuildResult, error) {
|
||||
var results []BuildResult
|
||||
for _, target := range targets {
|
||||
_, stderr, _, err := t.Run("go", "build", "-o", "/dev/null", target)
|
||||
r := BuildResult{Target: target}
|
||||
if err != nil {
|
||||
r.Error = fmt.Errorf("%s", strings.TrimSpace(stderr))
|
||||
}
|
||||
results = append(results, r)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// TestCount returns the number of test functions in a package.
|
||||
func (t *Toolkit) TestCount(pkg string) (int, error) {
|
||||
stdout, stderr, exitCode, err := t.Run("go", "test", "-list", ".*", pkg)
|
||||
if err != nil && exitCode != 0 {
|
||||
return 0, fmt.Errorf("go test -list failed: %s\n%s", err, stderr)
|
||||
}
|
||||
count := 0
|
||||
for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") {
|
||||
if strings.HasPrefix(line, "Test") || strings.HasPrefix(line, "Benchmark") {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Coverage runs go test -cover and parses per-package coverage percentages.
|
||||
func (t *Toolkit) Coverage(pkg string) ([]CoverageReport, error) {
|
||||
if pkg == "" {
|
||||
pkg = "./..."
|
||||
}
|
||||
stdout, stderr, exitCode, err := t.Run("go", "test", "-cover", pkg)
|
||||
if err != nil && exitCode != 0 && !strings.Contains(stdout, "coverage:") {
|
||||
return nil, fmt.Errorf("go test -cover failed (exit %d): %s\n%s", exitCode, err, stderr)
|
||||
}
|
||||
|
||||
var reports []CoverageReport
|
||||
re := regexp.MustCompile(`ok\s+(\S+)\s+.*coverage:\s+([\d.]+)%`)
|
||||
scanner := bufio.NewScanner(strings.NewReader(stdout))
|
||||
|
||||
for scanner.Scan() {
|
||||
matches := re.FindStringSubmatch(scanner.Text())
|
||||
if len(matches) == 3 {
|
||||
pct, _ := strconv.ParseFloat(matches[2], 64)
|
||||
reports = append(reports, CoverageReport{
|
||||
Package: matches[1],
|
||||
Percentage: pct,
|
||||
})
|
||||
}
|
||||
}
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// RaceDetect runs go test -race and parses data race warnings.
|
||||
func (t *Toolkit) RaceDetect(pkg string) ([]RaceCondition, error) {
|
||||
if pkg == "" {
|
||||
pkg = "./..."
|
||||
}
|
||||
_, stderr, _, err := t.Run("go", "test", "-race", pkg)
|
||||
if err != nil && !strings.Contains(stderr, "WARNING: DATA RACE") {
|
||||
return nil, fmt.Errorf("go test -race failed: %w", err)
|
||||
}
|
||||
|
||||
var races []RaceCondition
|
||||
lines := strings.Split(stderr, "\n")
|
||||
reFile := regexp.MustCompile(`\s+(.*\.go):(\d+)`)
|
||||
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "WARNING: DATA RACE") {
|
||||
rc := RaceCondition{Desc: "Data race detected"}
|
||||
for j := i + 1; j < len(lines) && j < i+15; j++ {
|
||||
if match := reFile.FindStringSubmatch(lines[j]); len(match) == 3 {
|
||||
rc.File = strings.TrimSpace(match[1])
|
||||
rc.Line, _ = strconv.Atoi(match[2])
|
||||
break
|
||||
}
|
||||
}
|
||||
races = append(races, rc)
|
||||
}
|
||||
}
|
||||
return races, nil
|
||||
}
|
||||
|
||||
// Complexity runs gocyclo and returns functions exceeding the threshold.
|
||||
func (t *Toolkit) Complexity(threshold int) ([]ComplexFunc, error) {
|
||||
stdout, stderr, exitCode, err := t.Run("gocyclo", "-over", strconv.Itoa(threshold), ".")
|
||||
if err != nil && exitCode == -1 {
|
||||
return nil, fmt.Errorf("gocyclo not available: %s\n%s", err, stderr)
|
||||
}
|
||||
|
||||
var funcs []ComplexFunc
|
||||
scanner := bufio.NewScanner(strings.NewReader(stdout))
|
||||
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 4 {
|
||||
continue
|
||||
}
|
||||
score, _ := strconv.Atoi(fields[0])
|
||||
fileParts := strings.Split(fields[3], ":")
|
||||
line := 0
|
||||
if len(fileParts) > 1 {
|
||||
line, _ = strconv.Atoi(fileParts[1])
|
||||
}
|
||||
|
||||
funcs = append(funcs, ComplexFunc{
|
||||
Score: score,
|
||||
Package: fields[1],
|
||||
FuncName: fields[2],
|
||||
File: fileParts[0],
|
||||
Line: line,
|
||||
})
|
||||
}
|
||||
return funcs, nil
|
||||
}
|
||||
|
||||
// DepGraph runs go mod graph and builds a dependency graph.
|
||||
func (t *Toolkit) DepGraph(pkg string) (*Graph, error) {
|
||||
stdout, stderr, exitCode, err := t.Run("go", "mod", "graph")
|
||||
if err != nil && exitCode != 0 {
|
||||
return nil, fmt.Errorf("go mod graph failed (exit %d): %s\n%s", exitCode, err, stderr)
|
||||
}
|
||||
|
||||
graph := &Graph{Edges: make(map[string][]string)}
|
||||
nodes := make(map[string]struct{})
|
||||
scanner := bufio.NewScanner(strings.NewReader(stdout))
|
||||
|
||||
for scanner.Scan() {
|
||||
parts := strings.Fields(scanner.Text())
|
||||
if len(parts) >= 2 {
|
||||
src, dst := parts[0], parts[1]
|
||||
graph.Edges[src] = append(graph.Edges[src], dst)
|
||||
nodes[src] = struct{}{}
|
||||
nodes[dst] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for node := range nodes {
|
||||
graph.Nodes = append(graph.Nodes, node)
|
||||
}
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
// GitLog returns the last n commits from git history.
|
||||
func (t *Toolkit) GitLog(n int) ([]Commit, error) {
|
||||
stdout, stderr, exitCode, err := t.Run("git", "log", fmt.Sprintf("-n%d", n), "--format=%H|%an|%aI|%s")
|
||||
if err != nil && exitCode != 0 {
|
||||
return nil, fmt.Errorf("git log failed (exit %d): %s\n%s", exitCode, err, stderr)
|
||||
}
|
||||
|
||||
var commits []Commit
|
||||
scanner := bufio.NewScanner(strings.NewReader(stdout))
|
||||
|
||||
for scanner.Scan() {
|
||||
parts := strings.SplitN(scanner.Text(), "|", 4)
|
||||
if len(parts) < 4 {
|
||||
continue
|
||||
}
|
||||
date, _ := time.Parse(time.RFC3339, parts[2])
|
||||
commits = append(commits, Commit{
|
||||
Hash: parts[0],
|
||||
Author: parts[1],
|
||||
Date: date,
|
||||
Message: parts[3],
|
||||
})
|
||||
}
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
// CheckPerms walks a directory and flags files with overly permissive modes.
|
||||
func (t *Toolkit) CheckPerms(dir string) ([]PermIssue, error) {
|
||||
var issues []PermIssue
|
||||
err := filepath.Walk(filepath.Join(t.Dir, dir), func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
mode := info.Mode().Perm()
|
||||
if mode&0o002 != 0 {
|
||||
issues = append(issues, PermIssue{
|
||||
File: path,
|
||||
Permission: fmt.Sprintf("%04o", mode),
|
||||
Issue: "World-writable",
|
||||
})
|
||||
} else if mode&0o020 != 0 && mode&0o002 != 0 {
|
||||
issues = append(issues, PermIssue{
|
||||
File: path,
|
||||
Permission: fmt.Sprintf("%04o", mode),
|
||||
Issue: "Group and world-writable",
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("walk failed: %w", err)
|
||||
}
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
// LEK-1 | lthn.ai | EUPL-1.2
|
||||
270
pkg/devkit/devkit_test.go
Normal file
270
pkg/devkit/devkit_test.go
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
// Designed by Gemini 3 Pro (Hypnos) + Claude Opus (Charon), signed LEK-1 | lthn.ai | EUPL-1.2
|
||||
package devkit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// setupMockCmd creates a shell script in a temp dir that echoes predetermined
|
||||
// content, and prepends that dir to PATH so Run() picks it up.
|
||||
func setupMockCmd(t *testing.T, name, content string) {
|
||||
t.Helper()
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, name)
|
||||
|
||||
script := fmt.Sprintf("#!/bin/sh\ncat <<'MOCK_EOF'\n%s\nMOCK_EOF\n", content)
|
||||
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
|
||||
t.Fatalf("failed to write mock command %s: %v", name, err)
|
||||
}
|
||||
|
||||
oldPath := os.Getenv("PATH")
|
||||
t.Setenv("PATH", tmpDir+string(os.PathListSeparator)+oldPath)
|
||||
}
|
||||
|
||||
// setupMockCmdExit creates a mock that echoes to stdout/stderr and exits with a code.
|
||||
func setupMockCmdExit(t *testing.T, name, stdout, stderr string, exitCode int) {
|
||||
t.Helper()
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, name)
|
||||
|
||||
script := fmt.Sprintf("#!/bin/sh\ncat <<'MOCK_EOF'\n%s\nMOCK_EOF\ncat <<'MOCK_ERR' >&2\n%s\nMOCK_ERR\nexit %d\n", stdout, stderr, exitCode)
|
||||
if err := os.WriteFile(scriptPath, []byte(script), 0755); err != nil {
|
||||
t.Fatalf("failed to write mock command %s: %v", name, err)
|
||||
}
|
||||
|
||||
oldPath := os.Getenv("PATH")
|
||||
t.Setenv("PATH", tmpDir+string(os.PathListSeparator)+oldPath)
|
||||
}
|
||||
|
||||
func TestCoverage_Good(t *testing.T) {
|
||||
output := `? example.com/skipped [no test files]
|
||||
ok example.com/pkg1 0.5s coverage: 85.0% of statements
|
||||
ok example.com/pkg2 0.2s coverage: 100.0% of statements`
|
||||
|
||||
setupMockCmd(t, "go", output)
|
||||
|
||||
tk := New(t.TempDir())
|
||||
reports, err := tk.Coverage("./...")
|
||||
if err != nil {
|
||||
t.Fatalf("Coverage failed: %v", err)
|
||||
}
|
||||
if len(reports) != 2 {
|
||||
t.Fatalf("expected 2 reports, got %d", len(reports))
|
||||
}
|
||||
if reports[0].Package != "example.com/pkg1" || reports[0].Percentage != 85.0 {
|
||||
t.Errorf("report 0: want pkg1@85%%, got %s@%.1f%%", reports[0].Package, reports[0].Percentage)
|
||||
}
|
||||
if reports[1].Package != "example.com/pkg2" || reports[1].Percentage != 100.0 {
|
||||
t.Errorf("report 1: want pkg2@100%%, got %s@%.1f%%", reports[1].Package, reports[1].Percentage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoverage_Bad(t *testing.T) {
|
||||
// No coverage lines in output
|
||||
setupMockCmd(t, "go", "FAIL\texample.com/broken [build failed]")
|
||||
|
||||
tk := New(t.TempDir())
|
||||
reports, err := tk.Coverage("./...")
|
||||
if err != nil {
|
||||
t.Fatalf("Coverage should not error on partial output: %v", err)
|
||||
}
|
||||
if len(reports) != 0 {
|
||||
t.Errorf("expected 0 reports from failed build, got %d", len(reports))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitLog_Good(t *testing.T) {
|
||||
now := time.Now().Truncate(time.Second)
|
||||
nowStr := now.Format(time.RFC3339)
|
||||
|
||||
output := fmt.Sprintf("abc123|Alice|%s|Fix the bug\ndef456|Bob|%s|Add feature", nowStr, nowStr)
|
||||
setupMockCmd(t, "git", output)
|
||||
|
||||
tk := New(t.TempDir())
|
||||
commits, err := tk.GitLog(2)
|
||||
if err != nil {
|
||||
t.Fatalf("GitLog failed: %v", err)
|
||||
}
|
||||
if len(commits) != 2 {
|
||||
t.Fatalf("expected 2 commits, got %d", len(commits))
|
||||
}
|
||||
if commits[0].Hash != "abc123" {
|
||||
t.Errorf("hash: want abc123, got %s", commits[0].Hash)
|
||||
}
|
||||
if commits[0].Author != "Alice" {
|
||||
t.Errorf("author: want Alice, got %s", commits[0].Author)
|
||||
}
|
||||
if commits[0].Message != "Fix the bug" {
|
||||
t.Errorf("message: want 'Fix the bug', got %q", commits[0].Message)
|
||||
}
|
||||
if !commits[0].Date.Equal(now) {
|
||||
t.Errorf("date: want %v, got %v", now, commits[0].Date)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitLog_Bad(t *testing.T) {
|
||||
// Malformed lines should be skipped
|
||||
setupMockCmd(t, "git", "incomplete|line\nabc|Bob|2025-01-01T00:00:00Z|Good commit")
|
||||
|
||||
tk := New(t.TempDir())
|
||||
commits, err := tk.GitLog(5)
|
||||
if err != nil {
|
||||
t.Fatalf("GitLog failed: %v", err)
|
||||
}
|
||||
if len(commits) != 1 {
|
||||
t.Errorf("expected 1 valid commit (skip malformed), got %d", len(commits))
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplexity_Good(t *testing.T) {
|
||||
output := "15 main ComplexFunc file.go:10:1\n20 pkg VeryComplex other.go:50:1"
|
||||
setupMockCmd(t, "gocyclo", output)
|
||||
|
||||
tk := New(t.TempDir())
|
||||
funcs, err := tk.Complexity(10)
|
||||
if err != nil {
|
||||
t.Fatalf("Complexity failed: %v", err)
|
||||
}
|
||||
if len(funcs) != 2 {
|
||||
t.Fatalf("expected 2 funcs, got %d", len(funcs))
|
||||
}
|
||||
if funcs[0].Score != 15 || funcs[0].FuncName != "ComplexFunc" || funcs[0].File != "file.go" || funcs[0].Line != 10 {
|
||||
t.Errorf("func 0: unexpected %+v", funcs[0])
|
||||
}
|
||||
if funcs[1].Score != 20 || funcs[1].Package != "pkg" {
|
||||
t.Errorf("func 1: unexpected %+v", funcs[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestComplexity_Bad(t *testing.T) {
|
||||
// No functions above threshold = empty output
|
||||
setupMockCmd(t, "gocyclo", "")
|
||||
|
||||
tk := New(t.TempDir())
|
||||
funcs, err := tk.Complexity(50)
|
||||
if err != nil {
|
||||
t.Fatalf("Complexity should not error on empty output: %v", err)
|
||||
}
|
||||
if len(funcs) != 0 {
|
||||
t.Errorf("expected 0 funcs, got %d", len(funcs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDepGraph_Good(t *testing.T) {
|
||||
output := "modA@v1 modB@v2\nmodA@v1 modC@v3\nmodB@v2 modD@v1"
|
||||
setupMockCmd(t, "go", output)
|
||||
|
||||
tk := New(t.TempDir())
|
||||
graph, err := tk.DepGraph("./...")
|
||||
if err != nil {
|
||||
t.Fatalf("DepGraph failed: %v", err)
|
||||
}
|
||||
if len(graph.Nodes) != 4 {
|
||||
t.Errorf("expected 4 nodes, got %d: %v", len(graph.Nodes), graph.Nodes)
|
||||
}
|
||||
edgesA := graph.Edges["modA@v1"]
|
||||
if len(edgesA) != 2 {
|
||||
t.Errorf("expected 2 edges from modA@v1, got %d", len(edgesA))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRaceDetect_Good(t *testing.T) {
|
||||
// No races = clean run
|
||||
setupMockCmd(t, "go", "ok\texample.com/safe\t0.1s")
|
||||
|
||||
tk := New(t.TempDir())
|
||||
races, err := tk.RaceDetect("./...")
|
||||
if err != nil {
|
||||
t.Fatalf("RaceDetect failed on clean run: %v", err)
|
||||
}
|
||||
if len(races) != 0 {
|
||||
t.Errorf("expected 0 races, got %d", len(races))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRaceDetect_Bad(t *testing.T) {
|
||||
stderrOut := `WARNING: DATA RACE
|
||||
Read at 0x00c000123456 by goroutine 7:
|
||||
/home/user/project/main.go:42
|
||||
Previous write at 0x00c000123456 by goroutine 6:
|
||||
/home/user/project/main.go:38`
|
||||
|
||||
setupMockCmdExit(t, "go", "", stderrOut, 1)
|
||||
|
||||
tk := New(t.TempDir())
|
||||
races, err := tk.RaceDetect("./...")
|
||||
if err != nil {
|
||||
t.Fatalf("RaceDetect should parse races, not error: %v", err)
|
||||
}
|
||||
if len(races) != 1 {
|
||||
t.Fatalf("expected 1 race, got %d", len(races))
|
||||
}
|
||||
if races[0].File != "/home/user/project/main.go" || races[0].Line != 42 {
|
||||
t.Errorf("race: unexpected %+v", races[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiffStat_Good(t *testing.T) {
|
||||
output := ` file1.go | 10 +++++++---
|
||||
file2.go | 5 +++++
|
||||
2 files changed, 12 insertions(+), 3 deletions(-)`
|
||||
setupMockCmd(t, "git", output)
|
||||
|
||||
tk := New(t.TempDir())
|
||||
s, err := tk.DiffStat()
|
||||
if err != nil {
|
||||
t.Fatalf("DiffStat failed: %v", err)
|
||||
}
|
||||
if s.FilesChanged != 2 {
|
||||
t.Errorf("files: want 2, got %d", s.FilesChanged)
|
||||
}
|
||||
if s.Insertions != 12 {
|
||||
t.Errorf("insertions: want 12, got %d", s.Insertions)
|
||||
}
|
||||
if s.Deletions != 3 {
|
||||
t.Errorf("deletions: want 3, got %d", s.Deletions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPerms_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create a world-writable file
|
||||
badFile := filepath.Join(dir, "bad.txt")
|
||||
if err := os.WriteFile(badFile, []byte("test"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chmod(badFile, 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Create a safe file
|
||||
goodFile := filepath.Join(dir, "good.txt")
|
||||
if err := os.WriteFile(goodFile, []byte("test"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tk := New("/")
|
||||
issues, err := tk.CheckPerms(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("CheckPerms failed: %v", err)
|
||||
}
|
||||
if len(issues) != 1 {
|
||||
t.Fatalf("expected 1 issue (world-writable), got %d", len(issues))
|
||||
}
|
||||
if issues[0].Issue != "World-writable" {
|
||||
t.Errorf("issue: want 'World-writable', got %q", issues[0].Issue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tk := New("/tmp")
|
||||
if tk.Dir != "/tmp" {
|
||||
t.Errorf("Dir: want /tmp, got %s", tk.Dir)
|
||||
}
|
||||
}
|
||||
|
||||
// LEK-1 | lthn.ai | EUPL-1.2
|
||||
|
|
@ -1146,277 +1146,129 @@
|
|||
"error.gh_not_found": "'gh' CLI not found. Install from https://cli.github.com/",
|
||||
"error.registry_not_found": "No repos.yaml found",
|
||||
"error.repo_not_found": "Repository '{{.Name}}' not found",
|
||||
"gram.article.definite": "the",
|
||||
"gram.article.definite.feminine": "",
|
||||
"gram.article.definite.masculine": "",
|
||||
"gram.article.definite.neuter": "",
|
||||
"gram.article.indefinite.default": "a",
|
||||
"gram.article.indefinite.feminine": "",
|
||||
"gram.article.indefinite.masculine": "",
|
||||
"gram.article.indefinite.neuter": "",
|
||||
"gram.article.indefinite.vowel": "an",
|
||||
"gram.noun.artifact.one": "artifact",
|
||||
"gram.noun.artifact.other": "artifacts",
|
||||
"gram.noun.branch.gender": "",
|
||||
"gram.noun.branch.one": "branch",
|
||||
"gram.noun.branch.other": "branches",
|
||||
"gram.noun.category.one": "category",
|
||||
"gram.noun.category.other": "categories",
|
||||
"gram.noun.change.gender": "",
|
||||
"gram.noun.change.one": "change",
|
||||
"gram.noun.change.other": "changes",
|
||||
"gram.noun.check.one": "check",
|
||||
"gram.noun.check.other": "checks",
|
||||
"gram.noun.child.one": "child",
|
||||
"gram.noun.child.other": "children",
|
||||
"gram.noun.commit.gender": "",
|
||||
"gram.noun.commit.one": "commit",
|
||||
"gram.noun.commit.other": "commits",
|
||||
"gram.noun.dependency.one": "dependency",
|
||||
"gram.noun.dependency.other": "dependencies",
|
||||
"gram.noun.directory.one": "directory",
|
||||
"gram.noun.directory.other": "directories",
|
||||
"gram.noun.failed.one": "failed",
|
||||
"gram.noun.failed.other": "failed",
|
||||
"gram.noun.file.gender": "",
|
||||
"gram.noun.file.one": "file",
|
||||
"gram.noun.file.other": "files",
|
||||
"gram.noun.issue.one": "issue",
|
||||
"gram.noun.issue.other": "issues",
|
||||
"gram.noun.item.gender": "",
|
||||
"gram.noun.item.one": "item",
|
||||
"gram.noun.item.other": "items",
|
||||
"gram.noun.package.one": "package",
|
||||
"gram.noun.package.other": "packages",
|
||||
"gram.noun.passed.one": "passed",
|
||||
"gram.noun.passed.other": "passed",
|
||||
"gram.noun.person.one": "person",
|
||||
"gram.noun.person.other": "people",
|
||||
"gram.noun.query.one": "query",
|
||||
"gram.noun.query.other": "queries",
|
||||
"gram.noun.repo.gender": "",
|
||||
"gram.noun.repo.one": "repo",
|
||||
"gram.noun.repo.other": "repos",
|
||||
"gram.noun.repository.one": "repository",
|
||||
"gram.noun.repository.other": "repositories",
|
||||
"gram.noun.skipped.one": "skipped",
|
||||
"gram.noun.skipped.other": "skipped",
|
||||
"gram.noun.task.one": "task",
|
||||
"gram.noun.task.other": "tasks",
|
||||
"gram.noun.test.one": "test",
|
||||
"gram.noun.test.other": "tests",
|
||||
"gram.noun.vulnerability.one": "vulnerability",
|
||||
"gram.noun.vulnerability.other": "vulnerabilities",
|
||||
"gram.number.decimal": ".",
|
||||
"gram.number.percent": "%s%%",
|
||||
"gram.number.thousands": ",",
|
||||
"gram.punct.label": ":",
|
||||
"gram.punct.progress": "...",
|
||||
"gram.verb.analyse.base": "",
|
||||
"gram.verb.analyse.gerund": "",
|
||||
"gram.verb.analyse.past": "",
|
||||
"gram.verb.be.base": "be",
|
||||
"gram.verb.be.gerund": "being",
|
||||
"gram.verb.be.past": "was",
|
||||
"gram.verb.begin.base": "begin",
|
||||
"gram.verb.begin.gerund": "beginning",
|
||||
"gram.verb.begin.past": "began",
|
||||
"gram.verb.bring.base": "bring",
|
||||
"gram.verb.bring.gerund": "bringing",
|
||||
"gram.verb.bring.past": "brought",
|
||||
"gram.verb.build.base": "build",
|
||||
"gram.verb.build.gerund": "building",
|
||||
"gram.verb.build.past": "built",
|
||||
"gram.verb.buy.base": "buy",
|
||||
"gram.verb.buy.gerund": "buying",
|
||||
"gram.verb.buy.past": "bought",
|
||||
"gram.verb.catch.base": "catch",
|
||||
"gram.verb.catch.gerund": "catching",
|
||||
"gram.verb.catch.past": "caught",
|
||||
"gram.verb.check.base": "",
|
||||
"gram.verb.check.gerund": "",
|
||||
"gram.verb.check.past": "",
|
||||
"gram.verb.choose.base": "choose",
|
||||
"gram.verb.choose.gerund": "choosing",
|
||||
"gram.verb.choose.past": "chose",
|
||||
"gram.verb.commit.base": "commit",
|
||||
"gram.verb.commit.gerund": "committing",
|
||||
"gram.verb.commit.past": "committed",
|
||||
"gram.verb.create.base": "",
|
||||
"gram.verb.create.gerund": "",
|
||||
"gram.verb.create.past": "",
|
||||
"gram.verb.cut.base": "cut",
|
||||
"gram.verb.cut.gerund": "cutting",
|
||||
"gram.verb.cut.past": "cut",
|
||||
"gram.verb.delete.base": "",
|
||||
"gram.verb.delete.gerund": "",
|
||||
"gram.verb.delete.past": "",
|
||||
"gram.verb.do.base": "do",
|
||||
"gram.verb.do.gerund": "doing",
|
||||
"gram.verb.do.past": "did",
|
||||
"gram.verb.find.base": "find",
|
||||
"gram.verb.find.gerund": "finding",
|
||||
"gram.verb.find.past": "found",
|
||||
"gram.verb.format.base": "format",
|
||||
"gram.verb.format.gerund": "formatting",
|
||||
"gram.verb.format.past": "formatted",
|
||||
"gram.verb.get.base": "get",
|
||||
"gram.verb.get.gerund": "getting",
|
||||
"gram.verb.get.past": "got",
|
||||
"gram.verb.go.base": "go",
|
||||
"gram.verb.go.gerund": "going",
|
||||
"gram.verb.go.past": "went",
|
||||
"gram.verb.have.base": "have",
|
||||
"gram.verb.have.gerund": "having",
|
||||
"gram.verb.have.past": "had",
|
||||
"gram.verb.hit.base": "hit",
|
||||
"gram.verb.hit.gerund": "hitting",
|
||||
"gram.verb.hit.past": "hit",
|
||||
"gram.verb.hold.base": "hold",
|
||||
"gram.verb.hold.gerund": "holding",
|
||||
"gram.verb.hold.past": "held",
|
||||
"gram.verb.install.base": "",
|
||||
"gram.verb.install.gerund": "",
|
||||
"gram.verb.install.past": "",
|
||||
"gram.verb.keep.base": "keep",
|
||||
"gram.verb.keep.gerund": "keeping",
|
||||
"gram.verb.keep.past": "kept",
|
||||
"gram.verb.lead.base": "lead",
|
||||
"gram.verb.lead.gerund": "leading",
|
||||
"gram.verb.lead.past": "led",
|
||||
"gram.verb.leave.base": "leave",
|
||||
"gram.verb.leave.gerund": "leaving",
|
||||
"gram.verb.leave.past": "left",
|
||||
"gram.verb.lose.base": "lose",
|
||||
"gram.verb.lose.gerund": "losing",
|
||||
"gram.verb.lose.past": "lost",
|
||||
"gram.verb.make.base": "make",
|
||||
"gram.verb.make.gerund": "making",
|
||||
"gram.verb.make.past": "made",
|
||||
"gram.verb.meet.base": "meet",
|
||||
"gram.verb.meet.gerund": "meeting",
|
||||
"gram.verb.meet.past": "met",
|
||||
"gram.verb.organise.base": "",
|
||||
"gram.verb.organise.gerund": "",
|
||||
"gram.verb.organise.past": "",
|
||||
"gram.verb.pay.base": "pay",
|
||||
"gram.verb.pay.gerund": "paying",
|
||||
"gram.verb.pay.past": "paid",
|
||||
"gram.verb.pull.base": "",
|
||||
"gram.verb.pull.gerund": "",
|
||||
"gram.verb.pull.past": "",
|
||||
"gram.verb.push.base": "",
|
||||
"gram.verb.push.gerund": "",
|
||||
"gram.verb.push.past": "",
|
||||
"gram.verb.put.base": "put",
|
||||
"gram.verb.put.gerund": "putting",
|
||||
"gram.verb.put.past": "put",
|
||||
"gram.verb.realise.base": "",
|
||||
"gram.verb.realise.gerund": "",
|
||||
"gram.verb.realise.past": "",
|
||||
"gram.verb.recognise.base": "",
|
||||
"gram.verb.recognise.gerund": "",
|
||||
"gram.verb.recognise.past": "",
|
||||
"gram.verb.run.base": "run",
|
||||
"gram.verb.run.gerund": "running",
|
||||
"gram.verb.run.past": "ran",
|
||||
"gram.verb.save.base": "",
|
||||
"gram.verb.save.gerund": "",
|
||||
"gram.verb.save.past": "",
|
||||
"gram.verb.scan.base": "scan",
|
||||
"gram.verb.scan.gerund": "scanning",
|
||||
"gram.verb.scan.past": "scanned",
|
||||
"gram.verb.sell.base": "sell",
|
||||
"gram.verb.sell.gerund": "selling",
|
||||
"gram.verb.sell.past": "sold",
|
||||
"gram.verb.send.base": "send",
|
||||
"gram.verb.send.gerund": "sending",
|
||||
"gram.verb.send.past": "sent",
|
||||
"gram.verb.set.base": "set",
|
||||
"gram.verb.set.gerund": "setting",
|
||||
"gram.verb.set.past": "set",
|
||||
"gram.verb.shut.base": "shut",
|
||||
"gram.verb.shut.gerund": "shutting",
|
||||
"gram.verb.shut.past": "shut",
|
||||
"gram.verb.sit.base": "sit",
|
||||
"gram.verb.sit.gerund": "sitting",
|
||||
"gram.verb.sit.past": "sat",
|
||||
"gram.verb.spend.base": "spend",
|
||||
"gram.verb.spend.gerund": "spending",
|
||||
"gram.verb.spend.past": "spent",
|
||||
"gram.verb.split.base": "split",
|
||||
"gram.verb.split.gerund": "splitting",
|
||||
"gram.verb.split.past": "split",
|
||||
"gram.verb.stop.base": "stop",
|
||||
"gram.verb.stop.gerund": "stopping",
|
||||
"gram.verb.stop.past": "stopped",
|
||||
"gram.verb.take.base": "take",
|
||||
"gram.verb.take.gerund": "taking",
|
||||
"gram.verb.take.past": "took",
|
||||
"gram.verb.think.base": "think",
|
||||
"gram.verb.think.gerund": "thinking",
|
||||
"gram.verb.think.past": "thought",
|
||||
"gram.verb.update.base": "",
|
||||
"gram.verb.update.gerund": "",
|
||||
"gram.verb.update.past": "",
|
||||
"gram.verb.win.base": "win",
|
||||
"gram.verb.win.gerund": "winning",
|
||||
"gram.verb.win.past": "won",
|
||||
"gram.verb.write.base": "write",
|
||||
"gram.verb.write.gerund": "writing",
|
||||
"gram.verb.write.past": "wrote",
|
||||
"gram.word.api": "API",
|
||||
"gram.word.app_url": "app URL",
|
||||
"gram.word.blocked_by": "blocked by",
|
||||
"gram.word.cgo": "CGO",
|
||||
"gram.word.ci": "CI",
|
||||
"gram.word.claimed_by": "claimed by",
|
||||
"gram.word.coverage": "coverage",
|
||||
"gram.word.cpus": "CPUs",
|
||||
"gram.word.dry_run": "dry run",
|
||||
"gram.word.failed": "failed",
|
||||
"gram.word.filter": "filter",
|
||||
"gram.word.go_mod": "go.mod",
|
||||
"gram.word.html": "HTML",
|
||||
"gram.word.id": "ID",
|
||||
"gram.word.ok": "OK",
|
||||
"gram.word.package": "package",
|
||||
"gram.word.passed": "passed",
|
||||
"gram.word.php": "PHP",
|
||||
"gram.word.pid": "PID",
|
||||
"gram.word.pnpm": "pnpm",
|
||||
"gram.word.pr": "PR",
|
||||
"gram.word.qa": "QA",
|
||||
"gram.word.related_files": "related files",
|
||||
"gram.word.sdk": "SDK",
|
||||
"gram.word.skipped": "skipped",
|
||||
"gram.word.ssh": "SSH",
|
||||
"gram.word.ssl": "SSL",
|
||||
"gram.word.test": "test",
|
||||
"gram.word.up_to_date": "up to date",
|
||||
"gram.word.url": "URL",
|
||||
"gram.word.vite": "Vite",
|
||||
"lang.de": "German",
|
||||
"lang.en": "English",
|
||||
"lang.es": "Spanish",
|
||||
"lang.fr": "French",
|
||||
"lang.zh": "Chinese",
|
||||
"prompt.confirm": "Are you sure?",
|
||||
"prompt.continue": "Continue?",
|
||||
"prompt.discard": "Discard changes?",
|
||||
"prompt.no": "n",
|
||||
"prompt.overwrite": "Overwrite?",
|
||||
"prompt.proceed": "Proceed?",
|
||||
"prompt.yes": "y",
|
||||
"time.ago.day.one": "{{.Count}} day ago",
|
||||
"time.ago.day.other": "{{.Count}} days ago",
|
||||
"time.ago.hour.one": "{{.Count}} hour ago",
|
||||
"time.ago.hour.other": "{{.Count}} hours ago",
|
||||
"time.ago.minute.one": "{{.Count}} minute ago",
|
||||
"time.ago.minute.other": "{{.Count}} minutes ago",
|
||||
"time.ago.second.one": "{{.Count}} second ago",
|
||||
"time.ago.second.other": "{{.Count}} seconds ago",
|
||||
"time.ago.week.one": "{{.Count}} week ago",
|
||||
"time.ago.week.other": "{{.Count}} weeks ago",
|
||||
"time.just_now": "just now"
|
||||
|
||||
"gram": {
|
||||
"verb": {
|
||||
"be": { "base": "be", "past": "was", "gerund": "being" },
|
||||
"go": { "base": "go", "past": "went", "gerund": "going" },
|
||||
"do": { "base": "do", "past": "did", "gerund": "doing" },
|
||||
"have": { "base": "have", "past": "had", "gerund": "having" },
|
||||
"make": { "base": "make", "past": "made", "gerund": "making" },
|
||||
"get": { "base": "get", "past": "got", "gerund": "getting" },
|
||||
"run": { "base": "run", "past": "ran", "gerund": "running" },
|
||||
"write": { "base": "write", "past": "wrote", "gerund": "writing" },
|
||||
"build": { "base": "build", "past": "built", "gerund": "building" },
|
||||
"send": { "base": "send", "past": "sent", "gerund": "sending" },
|
||||
"find": { "base": "find", "past": "found", "gerund": "finding" },
|
||||
"take": { "base": "take", "past": "took", "gerund": "taking" },
|
||||
"begin": { "base": "begin", "past": "began", "gerund": "beginning" },
|
||||
"keep": { "base": "keep", "past": "kept", "gerund": "keeping" },
|
||||
"hold": { "base": "hold", "past": "held", "gerund": "holding" },
|
||||
"bring": { "base": "bring", "past": "brought", "gerund": "bringing" },
|
||||
"think": { "base": "think", "past": "thought", "gerund": "thinking" },
|
||||
"buy": { "base": "buy", "past": "bought", "gerund": "buying" },
|
||||
"catch": { "base": "catch", "past": "caught", "gerund": "catching" },
|
||||
"choose": { "base": "choose", "past": "chose", "gerund": "choosing" },
|
||||
"lose": { "base": "lose", "past": "lost", "gerund": "losing" },
|
||||
"win": { "base": "win", "past": "won", "gerund": "winning" },
|
||||
"meet": { "base": "meet", "past": "met", "gerund": "meeting" },
|
||||
"lead": { "base": "lead", "past": "led", "gerund": "leading" },
|
||||
"leave": { "base": "leave", "past": "left", "gerund": "leaving" },
|
||||
"spend": { "base": "spend", "past": "spent", "gerund": "spending" },
|
||||
"pay": { "base": "pay", "past": "paid", "gerund": "paying" },
|
||||
"sell": { "base": "sell", "past": "sold", "gerund": "selling" },
|
||||
"commit": { "base": "commit", "past": "committed", "gerund": "committing" },
|
||||
"stop": { "base": "stop", "past": "stopped", "gerund": "stopping" },
|
||||
"scan": { "base": "scan", "past": "scanned", "gerund": "scanning" },
|
||||
"format": { "base": "format", "past": "formatted", "gerund": "formatting" },
|
||||
"set": { "base": "set", "past": "set", "gerund": "setting" },
|
||||
"put": { "base": "put", "past": "put", "gerund": "putting" },
|
||||
"cut": { "base": "cut", "past": "cut", "gerund": "cutting" },
|
||||
"hit": { "base": "hit", "past": "hit", "gerund": "hitting" },
|
||||
"sit": { "base": "sit", "past": "sat", "gerund": "sitting" },
|
||||
"split": { "base": "split", "past": "split", "gerund": "splitting" },
|
||||
"shut": { "base": "shut", "past": "shut", "gerund": "shutting" },
|
||||
"check": { "base": "check", "past": "checked", "gerund": "checking" },
|
||||
"create": { "base": "create", "past": "created", "gerund": "creating" },
|
||||
"delete": { "base": "delete", "past": "deleted", "gerund": "deleting" },
|
||||
"install": { "base": "install", "past": "installed", "gerund": "installing" },
|
||||
"update": { "base": "update", "past": "updated", "gerund": "updating" },
|
||||
"pull": { "base": "pull", "past": "pulled", "gerund": "pulling" },
|
||||
"push": { "base": "push", "past": "pushed", "gerund": "pushing" },
|
||||
"save": { "base": "save", "past": "saved", "gerund": "saving" },
|
||||
"analyse": { "base": "analyse", "past": "analysed", "gerund": "analysing" },
|
||||
"organise": { "base": "organise", "past": "organised", "gerund": "organising" },
|
||||
"realise": { "base": "realise", "past": "realised", "gerund": "realising" },
|
||||
"recognise": { "base": "recognise", "past": "recognised", "gerund": "recognising" }
|
||||
},
|
||||
"noun": {
|
||||
"file": { "one": "file", "other": "files" },
|
||||
"repo": { "one": "repo", "other": "repos" },
|
||||
"repository": { "one": "repository", "other": "repositories" },
|
||||
"commit": { "one": "commit", "other": "commits" },
|
||||
"branch": { "one": "branch", "other": "branches" },
|
||||
"change": { "one": "change", "other": "changes" },
|
||||
"item": { "one": "item", "other": "items" },
|
||||
"issue": { "one": "issue", "other": "issues" },
|
||||
"task": { "one": "task", "other": "tasks" },
|
||||
"person": { "one": "person", "other": "people" },
|
||||
"child": { "one": "child", "other": "children" },
|
||||
"package": { "one": "package", "other": "packages" },
|
||||
"artifact": { "one": "artifact", "other": "artifacts" },
|
||||
"vulnerability": { "one": "vulnerability", "other": "vulnerabilities" },
|
||||
"dependency": { "one": "dependency", "other": "dependencies" },
|
||||
"directory": { "one": "directory", "other": "directories" },
|
||||
"category": { "one": "category", "other": "categories" },
|
||||
"query": { "one": "query", "other": "queries" },
|
||||
"check": { "one": "check", "other": "checks" },
|
||||
"test": { "one": "test", "other": "tests" }
|
||||
},
|
||||
"article": {
|
||||
"indefinite": { "default": "a", "vowel": "an" },
|
||||
"definite": "the"
|
||||
},
|
||||
"word": {
|
||||
"url": "URL", "id": "ID", "ok": "OK", "ci": "CI", "qa": "QA",
|
||||
"php": "PHP", "sdk": "SDK", "html": "HTML", "cgo": "CGO", "pid": "PID",
|
||||
"cpus": "CPUs", "ssh": "SSH", "ssl": "SSL", "api": "API", "pr": "PR",
|
||||
"vite": "Vite", "pnpm": "pnpm",
|
||||
"app_url": "app URL", "blocked_by": "blocked by", "claimed_by": "claimed by",
|
||||
"related_files": "related files", "up_to_date": "up to date",
|
||||
"dry_run": "dry run", "go_mod": "go.mod",
|
||||
"coverage": "coverage", "failed": "failed", "filter": "filter",
|
||||
"package": "package", "passed": "passed", "skipped": "skipped", "test": "test"
|
||||
},
|
||||
"punct": {
|
||||
"label": ":",
|
||||
"progress": "..."
|
||||
},
|
||||
"number": {
|
||||
"thousands": ",",
|
||||
"decimal": ".",
|
||||
"percent": "%s%%"
|
||||
}
|
||||
},
|
||||
|
||||
"lang": {
|
||||
"de": "German", "en": "English", "es": "Spanish",
|
||||
"fr": "French", "ru": "Russian", "zh": "Chinese"
|
||||
},
|
||||
|
||||
"prompt": {
|
||||
"yes": "y", "no": "n",
|
||||
"continue": "Continue?", "proceed": "Proceed?",
|
||||
"confirm": "Are you sure?", "overwrite": "Overwrite?",
|
||||
"discard": "Discard changes?"
|
||||
},
|
||||
|
||||
"time": {
|
||||
"just_now": "just now",
|
||||
"ago": {
|
||||
"second": { "one": "{{.Count}} second ago", "other": "{{.Count}} seconds ago" },
|
||||
"minute": { "one": "{{.Count}} minute ago", "other": "{{.Count}} minutes ago" },
|
||||
"hour": { "one": "{{.Count}} hour ago", "other": "{{.Count}} hours ago" },
|
||||
"day": { "one": "{{.Count}} day ago", "other": "{{.Count}} days ago" },
|
||||
"week": { "one": "{{.Count}} week ago", "other": "{{.Count}} weeks ago" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1 +1,148 @@
|
|||
{}
|
||||
{
|
||||
"gram": {
|
||||
"verb": {
|
||||
"be": { "base": "是", "past": "是", "gerund": "状态" },
|
||||
"go": { "base": "前往", "past": "前往", "gerund": "前往" },
|
||||
"do": { "base": "执行", "past": "执行", "gerund": "执行" },
|
||||
"have": { "base": "拥有", "past": "拥有", "gerund": "拥有" },
|
||||
"make": { "base": "创建", "past": "创建", "gerund": "创建" },
|
||||
"get": { "base": "获取", "past": "获取", "gerund": "获取" },
|
||||
"run": { "base": "运行", "past": "运行", "gerund": "运行" },
|
||||
"write": { "base": "写入", "past": "写入", "gerund": "写入" },
|
||||
"build": { "base": "构建", "past": "构建", "gerund": "构建" },
|
||||
"send": { "base": "发送", "past": "发送", "gerund": "发送" },
|
||||
"find": { "base": "查找", "past": "查找", "gerund": "查找" },
|
||||
"take": { "base": "获取", "past": "获取", "gerund": "获取" },
|
||||
"begin": { "base": "开始", "past": "开始", "gerund": "开始" },
|
||||
"keep": { "base": "保持", "past": "保持", "gerund": "保持" },
|
||||
"hold": { "base": "持有", "past": "持有", "gerund": "持有" },
|
||||
"bring": { "base": "带来", "past": "带来", "gerund": "带来" },
|
||||
"think": { "base": "思考", "past": "思考", "gerund": "思考" },
|
||||
"choose": { "base": "选择", "past": "选择", "gerund": "选择" },
|
||||
"lose": { "base": "丢失", "past": "丢失", "gerund": "丢失" },
|
||||
"win": { "base": "成功", "past": "成功", "gerund": "成功" },
|
||||
"meet": { "base": "匹配", "past": "匹配", "gerund": "匹配" },
|
||||
"lead": { "base": "引导", "past": "引导", "gerund": "引导" },
|
||||
"leave": { "base": "离开", "past": "离开", "gerund": "离开" },
|
||||
"commit": { "base": "提交", "past": "提交", "gerund": "提交" },
|
||||
"stop": { "base": "停止", "past": "停止", "gerund": "停止" },
|
||||
"scan": { "base": "扫描", "past": "扫描", "gerund": "扫描" },
|
||||
"format": { "base": "格式化", "past": "格式化", "gerund": "格式化" },
|
||||
"set": { "base": "设置", "past": "设置", "gerund": "设置" },
|
||||
"check": { "base": "检查", "past": "检查", "gerund": "检查" },
|
||||
"create": { "base": "创建", "past": "创建", "gerund": "创建" },
|
||||
"delete": { "base": "删除", "past": "删除", "gerund": "删除" },
|
||||
"install": { "base": "安装", "past": "安装", "gerund": "安装" },
|
||||
"update": { "base": "更新", "past": "更新", "gerund": "更新" },
|
||||
"pull": { "base": "拉取", "past": "拉取", "gerund": "拉取" },
|
||||
"push": { "base": "推送", "past": "推送", "gerund": "推送" },
|
||||
"save": { "base": "保存", "past": "保存", "gerund": "保存" },
|
||||
"analyse": { "base": "分析", "past": "分析", "gerund": "分析" },
|
||||
"organise": { "base": "整理", "past": "整理", "gerund": "整理" },
|
||||
"test": { "base": "测试", "past": "测试", "gerund": "测试" },
|
||||
"deploy": { "base": "部署", "past": "部署", "gerund": "部署" },
|
||||
"clone": { "base": "克隆", "past": "克隆", "gerund": "克隆" },
|
||||
"compile": { "base": "编译", "past": "编译", "gerund": "编译" },
|
||||
"download": { "base": "下载", "past": "下载", "gerund": "下载" },
|
||||
"upload": { "base": "上传", "past": "上传", "gerund": "上传" }
|
||||
},
|
||||
"noun": {
|
||||
"file": { "one": "文件", "other": "文件" },
|
||||
"repo": { "one": "仓库", "other": "仓库" },
|
||||
"repository": { "one": "仓库", "other": "仓库" },
|
||||
"commit": { "one": "提交", "other": "提交" },
|
||||
"branch": { "one": "分支", "other": "分支" },
|
||||
"change": { "one": "更改", "other": "更改" },
|
||||
"item": { "one": "项", "other": "项" },
|
||||
"issue": { "one": "问题", "other": "问题" },
|
||||
"task": { "one": "任务", "other": "任务" },
|
||||
"person": { "one": "人", "other": "人" },
|
||||
"child": { "one": "子项", "other": "子项" },
|
||||
"package": { "one": "包", "other": "包" },
|
||||
"artifact": { "one": "构件", "other": "构件" },
|
||||
"vulnerability": { "one": "漏洞", "other": "漏洞" },
|
||||
"dependency": { "one": "依赖", "other": "依赖" },
|
||||
"directory": { "one": "目录", "other": "目录" },
|
||||
"category": { "one": "分类", "other": "分类" },
|
||||
"query": { "one": "查询", "other": "查询" },
|
||||
"check": { "one": "检查", "other": "检查" },
|
||||
"test": { "one": "测试", "other": "测试" },
|
||||
"error": { "one": "错误", "other": "错误" },
|
||||
"warning": { "one": "警告", "other": "警告" },
|
||||
"service": { "one": "服务", "other": "服务" },
|
||||
"config": { "one": "配置", "other": "配置" },
|
||||
"workflow": { "one": "工作流", "other": "工作流" }
|
||||
},
|
||||
"article": {
|
||||
"indefinite": { "default": "", "vowel": "" },
|
||||
"definite": ""
|
||||
},
|
||||
"word": {
|
||||
"url": "URL", "id": "ID", "ok": "OK", "ci": "CI", "qa": "QA",
|
||||
"php": "PHP", "sdk": "SDK", "html": "HTML", "cgo": "CGO", "pid": "PID",
|
||||
"cpus": "CPU", "ssh": "SSH", "ssl": "SSL", "api": "API", "pr": "PR",
|
||||
"vite": "Vite", "pnpm": "pnpm",
|
||||
"app_url": "应用 URL", "blocked_by": "被阻塞",
|
||||
"claimed_by": "已认领", "related_files": "相关文件",
|
||||
"up_to_date": "已是最新", "dry_run": "模拟运行",
|
||||
"go_mod": "go.mod", "coverage": "覆盖率", "failed": "失败",
|
||||
"filter": "过滤器", "package": "包", "passed": "通过",
|
||||
"skipped": "跳过", "test": "测试"
|
||||
},
|
||||
"punct": {
|
||||
"label": ":",
|
||||
"progress": "..."
|
||||
},
|
||||
"number": {
|
||||
"thousands": ",",
|
||||
"decimal": ".",
|
||||
"percent": "%s%%"
|
||||
}
|
||||
},
|
||||
|
||||
"cli.aborted": "已中止。",
|
||||
"cli.fail": "失败",
|
||||
"cli.pass": "通过",
|
||||
|
||||
"lang": {
|
||||
"de": "德语", "en": "英语", "es": "西班牙语",
|
||||
"fr": "法语", "ru": "俄语", "zh": "中文"
|
||||
},
|
||||
|
||||
"prompt": {
|
||||
"yes": "是", "no": "否",
|
||||
"continue": "继续?", "proceed": "执行?",
|
||||
"confirm": "确定吗?", "overwrite": "覆盖?",
|
||||
"discard": "放弃更改?"
|
||||
},
|
||||
|
||||
"time": {
|
||||
"just_now": "刚刚",
|
||||
"ago": {
|
||||
"second": { "other": "{{.Count}} 秒前" },
|
||||
"minute": { "other": "{{.Count}} 分钟前" },
|
||||
"hour": { "other": "{{.Count}} 小时前" },
|
||||
"day": { "other": "{{.Count}} 天前" },
|
||||
"week": { "other": "{{.Count}} 周前" }
|
||||
}
|
||||
},
|
||||
|
||||
"error.gh_not_found": "未找到 'gh' CLI 工具。请安装:https://cli.github.com/",
|
||||
"error.registry_not_found": "未找到 repos.yaml",
|
||||
"error.repo_not_found": "未找到仓库 '{{.Name}}'",
|
||||
|
||||
"common.label.done": "完成",
|
||||
"common.label.error": "错误",
|
||||
"common.label.info": "信息",
|
||||
"common.label.success": "成功",
|
||||
"common.label.warning": "警告",
|
||||
"common.status.clean": "干净",
|
||||
"common.status.dirty": "已修改",
|
||||
"common.status.running": "运行中",
|
||||
"common.status.stopped": "已停止",
|
||||
"common.status.up_to_date": "已是最新",
|
||||
"common.result.all_passed": "所有测试通过",
|
||||
"common.result.no_issues": "未发现问题",
|
||||
"common.prompt.abort": "已中止。",
|
||||
"common.success.completed": "{{.Action}} 成功完成"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue