carson 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/copilot-instructions.md +12 -0
- data/.github/pull_request_template.md +14 -0
- data/.github/workflows/carson_policy.yml +90 -0
- data/API.md +114 -0
- data/LICENSE +21 -0
- data/MANUAL.md +170 -0
- data/README.md +48 -0
- data/RELEASE.md +592 -0
- data/VERSION +1 -0
- data/assets/hooks/pre-commit +19 -0
- data/assets/hooks/pre-merge-commit +8 -0
- data/assets/hooks/pre-push +13 -0
- data/assets/hooks/prepare-commit-msg +8 -0
- data/carson.gemspec +37 -0
- data/exe/carson +13 -0
- data/lib/carson/adapters/git.rb +20 -0
- data/lib/carson/adapters/github.rb +20 -0
- data/lib/carson/cli.rb +189 -0
- data/lib/carson/config.rb +348 -0
- data/lib/carson/policy/ruby/lint.rb +61 -0
- data/lib/carson/runtime/audit.rb +793 -0
- data/lib/carson/runtime/lint.rb +177 -0
- data/lib/carson/runtime/local.rb +661 -0
- data/lib/carson/runtime/review/data_access.rb +253 -0
- data/lib/carson/runtime/review/gate_support.rb +224 -0
- data/lib/carson/runtime/review/query_text.rb +164 -0
- data/lib/carson/runtime/review/sweep_support.rb +252 -0
- data/lib/carson/runtime/review/utility.rb +63 -0
- data/lib/carson/runtime/review.rb +182 -0
- data/lib/carson/runtime.rb +182 -0
- data/lib/carson/version.rb +4 -0
- data/lib/carson.rb +6 -0
- data/templates/.github/copilot-instructions.md +12 -0
- data/templates/.github/pull_request_template.md +14 -0
- metadata +80 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
|
|
5
|
+
module Carson
|
|
6
|
+
class Runtime
|
|
7
|
+
module Lint
|
|
8
|
+
# Prepares canonical lint policy files under ~/AI/CODING from an explicit source.
|
|
9
|
+
def lint_setup!( source:, ref: "main", force: false )
|
|
10
|
+
print_header "Lint Setup"
|
|
11
|
+
source_text = source.to_s.strip
|
|
12
|
+
if source_text.empty?
|
|
13
|
+
puts_line "ERROR: lint setup requires --source <path-or-git-url>."
|
|
14
|
+
return EXIT_ERROR
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
ref_text = ref.to_s.strip
|
|
18
|
+
ref_text = "main" if ref_text.empty?
|
|
19
|
+
source_dir, cleanup = lint_setup_source_directory( source: source_text, ref: ref_text )
|
|
20
|
+
begin
|
|
21
|
+
source_coding_dir = File.join( source_dir, "CODING" )
|
|
22
|
+
unless Dir.exist?( source_coding_dir )
|
|
23
|
+
puts_line "ERROR: source CODING directory not found at #{source_coding_dir}."
|
|
24
|
+
return EXIT_ERROR
|
|
25
|
+
end
|
|
26
|
+
target_coding_dir = ai_coding_dir
|
|
27
|
+
copy_result = copy_lint_coding_tree(
|
|
28
|
+
source_coding_dir: source_coding_dir,
|
|
29
|
+
target_coding_dir: target_coding_dir,
|
|
30
|
+
force: force
|
|
31
|
+
)
|
|
32
|
+
puts_line "lint_setup_source: #{source_text}"
|
|
33
|
+
puts_line "lint_setup_ref: #{ref_text}" if lint_source_git_url?( source: source_text )
|
|
34
|
+
puts_line "lint_setup_target: #{target_coding_dir}"
|
|
35
|
+
puts_line "lint_setup_created: #{copy_result.fetch( :created )}"
|
|
36
|
+
puts_line "lint_setup_updated: #{copy_result.fetch( :updated )}"
|
|
37
|
+
puts_line "lint_setup_skipped: #{copy_result.fetch( :skipped )}"
|
|
38
|
+
|
|
39
|
+
missing_policy = missing_lint_policy_files
|
|
40
|
+
if missing_policy.empty?
|
|
41
|
+
puts_line "OK: lint policy setup is complete."
|
|
42
|
+
return EXIT_OK
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
missing_policy.each do |entry|
|
|
46
|
+
puts_line "missing_lint_policy_file: language=#{entry.fetch( :language )} path=#{entry.fetch( :path )}"
|
|
47
|
+
end
|
|
48
|
+
puts_line "ACTION: update source CODING policy files, rerun carson lint setup, then rerun carson audit."
|
|
49
|
+
EXIT_ERROR
|
|
50
|
+
ensure
|
|
51
|
+
cleanup&.call
|
|
52
|
+
end
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
puts_line "ERROR: lint setup failed (#{e.message})"
|
|
55
|
+
EXIT_ERROR
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
def lint_setup_source_directory( source:, ref: )
|
|
60
|
+
if lint_source_git_url?( source: source )
|
|
61
|
+
return lint_setup_clone_source( source: source, ref: ref )
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
expanded_source = File.expand_path( source )
|
|
65
|
+
raise "source path does not exist: #{expanded_source}" unless Dir.exist?( expanded_source )
|
|
66
|
+
[ expanded_source, nil ]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def lint_source_git_url?( source: )
|
|
70
|
+
text = source.to_s.strip
|
|
71
|
+
text.start_with?( "https://", "http://", "ssh://", "git@", "file://" )
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def lint_setup_clone_source( source:, ref: )
|
|
75
|
+
cache_root = cache_workspace_root
|
|
76
|
+
FileUtils.mkdir_p( cache_root )
|
|
77
|
+
work_dir = Dir.mktmpdir( "carson-lint-setup-", cache_root )
|
|
78
|
+
checkout_dir = File.join( work_dir, "source" )
|
|
79
|
+
clone_source = authenticated_lint_source( source: source )
|
|
80
|
+
stdout_text, stderr_text, status = Open3.capture3(
|
|
81
|
+
"git", "clone", "--depth", "1", "--branch", ref, clone_source, checkout_dir
|
|
82
|
+
)
|
|
83
|
+
unless status.success?
|
|
84
|
+
error_text = [ stderr_text.to_s.strip, stdout_text.to_s.strip ].reject( &:empty? ).join( " | " )
|
|
85
|
+
error_text = "git clone failed" if error_text.empty?
|
|
86
|
+
raise "unable to clone lint source #{safe_lint_source( source: source )} (#{error_text})"
|
|
87
|
+
end
|
|
88
|
+
[ checkout_dir, -> { FileUtils.rm_rf( work_dir ) } ]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def authenticated_lint_source( source: )
|
|
92
|
+
token = ENV.fetch( "CARSON_READ_TOKEN", "" ).to_s.strip
|
|
93
|
+
return source if token.empty?
|
|
94
|
+
|
|
95
|
+
return source unless source.start_with?( "https://github.com/", "http://github.com/", "git@github.com:" )
|
|
96
|
+
|
|
97
|
+
if source.start_with?( "git@github.com:" )
|
|
98
|
+
path = source.sub( "git@github.com:", "" )
|
|
99
|
+
return "https://x-access-token:#{token}@github.com/#{path}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
source.sub( %r{\Ahttps?://github\.com/}, "https://x-access-token:#{token}@github.com/" )
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def safe_lint_source( source: )
|
|
106
|
+
source.to_s.gsub( %r{https://[^@]+@}, "https://***@" )
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def cache_workspace_root
|
|
110
|
+
home = ENV.fetch( "HOME", "" ).to_s.strip
|
|
111
|
+
if home.start_with?( "/" )
|
|
112
|
+
path = File.join( home, ".cache", "carson" )
|
|
113
|
+
FileUtils.mkdir_p( path )
|
|
114
|
+
return path
|
|
115
|
+
end
|
|
116
|
+
"/tmp/carson"
|
|
117
|
+
rescue StandardError
|
|
118
|
+
"/tmp/carson"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def ai_coding_dir
|
|
122
|
+
home = ENV.fetch( "HOME", "" ).to_s.strip
|
|
123
|
+
raise "HOME must be an absolute path for lint setup" unless home.start_with?( "/" )
|
|
124
|
+
|
|
125
|
+
File.join( home, "AI", "CODING" )
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def copy_lint_coding_tree( source_coding_dir:, target_coding_dir:, force: )
|
|
129
|
+
FileUtils.mkdir_p( target_coding_dir )
|
|
130
|
+
created = 0
|
|
131
|
+
updated = 0
|
|
132
|
+
skipped = 0
|
|
133
|
+
Dir.glob( "**/*", File::FNM_DOTMATCH, base: source_coding_dir ).sort.each do |relative|
|
|
134
|
+
next if [ ".", ".." ].include?( relative )
|
|
135
|
+
source_path = File.join( source_coding_dir, relative )
|
|
136
|
+
target_path = File.join( target_coding_dir, relative )
|
|
137
|
+
if File.directory?( source_path )
|
|
138
|
+
FileUtils.mkdir_p( target_path )
|
|
139
|
+
next
|
|
140
|
+
end
|
|
141
|
+
next unless File.file?( source_path )
|
|
142
|
+
|
|
143
|
+
if File.exist?( target_path ) && !force
|
|
144
|
+
skipped += 1
|
|
145
|
+
next
|
|
146
|
+
end
|
|
147
|
+
target_exists = File.exist?( target_path )
|
|
148
|
+
FileUtils.mkdir_p( File.dirname( target_path ) )
|
|
149
|
+
FileUtils.cp( source_path, target_path )
|
|
150
|
+
FileUtils.chmod( File.stat( source_path ).mode & 0o777, target_path )
|
|
151
|
+
if target_exists
|
|
152
|
+
updated += 1
|
|
153
|
+
else
|
|
154
|
+
created += 1
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
{
|
|
158
|
+
created: created,
|
|
159
|
+
updated: updated,
|
|
160
|
+
skipped: skipped
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def missing_lint_policy_files
|
|
165
|
+
config.lint_languages.each_with_object( [] ) do |( language, entry ), missing|
|
|
166
|
+
next unless entry.fetch( :enabled )
|
|
167
|
+
|
|
168
|
+
entry.fetch( :config_files ).each do |path|
|
|
169
|
+
missing << { language: language, path: path } unless File.file?( path )
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
include Lint
|
|
176
|
+
end
|
|
177
|
+
end
|