simple_english 0.6.0-x86_64-linux
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/LICENSE +21 -0
- data/README.md +212 -0
- data/THIRD_PARTY_NOTICES.md +72 -0
- data/bin/se +6 -0
- data/docs/RULES.md +496 -0
- data/lib/simple_english/cli.rb +183 -0
- data/lib/simple_english/client/daemon.rb +233 -0
- data/lib/simple_english/client/language_tool.rb +95 -0
- data/lib/simple_english/daemon/engine.rb +40 -0
- data/lib/simple_english/daemon/http.rb +82 -0
- data/lib/simple_english/daemon/server.rb +285 -0
- data/lib/simple_english/lint/annotated_text.rb +117 -0
- data/lib/simple_english/lint/counts.rb +40 -0
- data/lib/simple_english/lint/extractor.rb +60 -0
- data/lib/simple_english/lint/finding.rb +24 -0
- data/lib/simple_english/lint/lint_plan.rb +22 -0
- data/lib/simple_english/lint/markdown.rb +121 -0
- data/lib/simple_english/lint/paragraph.rb +10 -0
- data/lib/simple_english/lint/plain_text.rb +30 -0
- data/lib/simple_english/lint/result.rb +18 -0
- data/lib/simple_english/lint/segment.rb +11 -0
- data/lib/simple_english/lint/span.rb +8 -0
- data/lib/simple_english/lint/suppressions.rb +37 -0
- data/lib/simple_english/model_context_protocol.rb +134 -0
- data/lib/simple_english/setup/config.rb +63 -0
- data/lib/simple_english/setup/fingerprint.rb +38 -0
- data/lib/simple_english/setup/install.rb +40 -0
- data/lib/simple_english/setup/languagetool.rb +23 -0
- data/lib/simple_english/version.rb +5 -0
- data/lib/simple_english.rb +60 -0
- data/libexec/simple_english/languagetool-server +4 -0
- data/libexec/simple_english/languagetool-server.sbom.json +9044 -0
- data/licenses/LGPL-2.1.txt +501 -0
- data/licenses/OpenJDK-LICENSE.txt +347 -0
- data/licenses/languagetool-resource-en/12dicts-readme.html +738 -0
- data/licenses/languagetool-resource-en/agid-readme.txt +340 -0
- data/licenses/languagetool-resource-en/pos-readme.txt +115 -0
- data/rules/simple-english.xml +621 -0
- metadata +185 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleEnglish
|
|
4
|
+
# The plain-text payload for /v2/check, duck-compatible with
|
|
5
|
+
# AnnotatedText::Result: #lt_params and #locate.
|
|
6
|
+
PlainText = Struct.new(:text) do
|
|
7
|
+
def lt_params
|
|
8
|
+
{"text" => text}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# LanguageTool reports offsets in Java UTF-16 code units.
|
|
12
|
+
# Return a 1-based line and UTF-16 column for a 0-based offset.
|
|
13
|
+
def locate(utf16_offset)
|
|
14
|
+
line = 1
|
|
15
|
+
column = 1
|
|
16
|
+
units = 0
|
|
17
|
+
text.each_char do |char|
|
|
18
|
+
return [line, column] if units >= utf16_offset
|
|
19
|
+
units += (char.ord > 0xFFFF) ? 2 : 1
|
|
20
|
+
if char == "\n"
|
|
21
|
+
line += 1
|
|
22
|
+
column = 1
|
|
23
|
+
else
|
|
24
|
+
column += (char.ord > 0xFFFF) ? 2 : 1
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
[line, column]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleEnglish
|
|
4
|
+
module AnnotatedText
|
|
5
|
+
# The payload for code comments. Duck interface shared with
|
|
6
|
+
# PlainText: #lt_params (the form data for LanguageTool)
|
|
7
|
+
# and #locate (a match offset back to file line and column).
|
|
8
|
+
Result = Struct.new(:source, :segments, :stream) do
|
|
9
|
+
def lt_params
|
|
10
|
+
{"data" => SimpleEnglish::AnnotatedText.data_json(self)}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def locate(utf16_offset)
|
|
14
|
+
SimpleEnglish::AnnotatedText.locate(self, utf16_offset)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleEnglish
|
|
4
|
+
module AnnotatedText
|
|
5
|
+
# One piece of the annotation stream: checkable comment text or
|
|
6
|
+
# uncheckable markup (markers, closers, gaps between comments).
|
|
7
|
+
# stream_start is the segment's offset into Result#stream.
|
|
8
|
+
Segment = Struct.new(:text, :content, :file_char, :stream_start,
|
|
9
|
+
:interpret_as)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Inline suppressions. A line containing `se: ignore` (or
|
|
4
|
+
# `se: ignore=RULE1,RULE2`) suppresses findings reported on that
|
|
5
|
+
# same line. Pattern findings cite the line of the match, not the
|
|
6
|
+
# paragraph or comment start, so put the directive on the line the
|
|
7
|
+
# finding reports.
|
|
8
|
+
|
|
9
|
+
module SimpleEnglish
|
|
10
|
+
module Suppressions
|
|
11
|
+
DIRECTIVE = /se:\s*ignore\s*(?:=\s*(?<rules>[\w,]+))?/
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def filter(source, findings)
|
|
16
|
+
return findings if findings.empty?
|
|
17
|
+
blocked = directives(source)
|
|
18
|
+
return findings if blocked.empty?
|
|
19
|
+
findings.reject do |finding|
|
|
20
|
+
blocked.any? do |directive|
|
|
21
|
+
directive[:line] == finding.line &&
|
|
22
|
+
(directive[:rules].empty? || directive[:rules].include?(finding.rule))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def directives(source)
|
|
28
|
+
source.lines.each_with_index.filter_map do |line, index|
|
|
29
|
+
next unless (match = line.match(DIRECTIVE))
|
|
30
|
+
rules = match[:rules] ? match[:rules].split(",").map(&:strip) : []
|
|
31
|
+
{line: index + 1, rules: rules}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private_class_method :directives
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
# MCP JSON-RPC server for the lint engine. `respond` is pure: request
|
|
6
|
+
# Hash in, response Hash or nil (notifications) out. `run` drives it
|
|
7
|
+
# over stdio, one JSON-RPC message per line. Protocol facts:
|
|
8
|
+
# integrations/verification.md.
|
|
9
|
+
|
|
10
|
+
module SimpleEnglish
|
|
11
|
+
module ModelContextProtocol
|
|
12
|
+
SUPPORTED_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"].freeze
|
|
13
|
+
LATEST_VERSION = "2025-06-18"
|
|
14
|
+
SERVER_NAME = "simple-english"
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def run(io: $stdin, out: $stdout, linter: method(:default_linter))
|
|
19
|
+
# A piped stdout buffers until 4 KB or EOF. An MCP client keeps
|
|
20
|
+
# stdin open while it waits for the answer, so an unflushed
|
|
21
|
+
# response never arrives and the client drops the server.
|
|
22
|
+
out.sync = true
|
|
23
|
+
while (line = io.gets)
|
|
24
|
+
line = line.strip
|
|
25
|
+
next if line.empty?
|
|
26
|
+
begin
|
|
27
|
+
request = JSON.parse(line)
|
|
28
|
+
rescue JSON::ParserError
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
next unless request.is_a?(Hash)
|
|
32
|
+
response =
|
|
33
|
+
begin
|
|
34
|
+
respond(request, linter: linter)
|
|
35
|
+
rescue => e
|
|
36
|
+
# A broken tool call (unreadable file, daemon hiccup) must not
|
|
37
|
+
# kill the loop: answer it, then serve the next line.
|
|
38
|
+
request.key?("id") ? error(request["id"], -32603, "lint failed: #{e.class}: #{e.message}") : nil
|
|
39
|
+
end
|
|
40
|
+
out.puts JSON.generate(response) if response
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The CLI's lint pipeline as a callable: .simple-english.yml decides
|
|
45
|
+
# which paths and rules count, then the daemon lints the file.
|
|
46
|
+
def default_linter(path)
|
|
47
|
+
config = SimpleEnglish::Config.load
|
|
48
|
+
return [] if SimpleEnglish::Config.ignore?(config, path)
|
|
49
|
+
findings = SimpleEnglish.lint_file(path)
|
|
50
|
+
findings.nil? ? nil : SimpleEnglish::Config.filter(config, path, findings)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def respond(request, linter:)
|
|
54
|
+
return nil unless request.key?("id")
|
|
55
|
+
id = request.fetch("id")
|
|
56
|
+
method = request["method"]
|
|
57
|
+
return error(id, -32600, "Invalid Request") unless method.is_a?(String)
|
|
58
|
+
params = request["params"]
|
|
59
|
+
params = {} unless params.is_a?(Hash)
|
|
60
|
+
case method
|
|
61
|
+
when "initialize" then ok(id, initialize_result(params["protocolVersion"]))
|
|
62
|
+
when "tools/list" then ok(id, {"tools" => [lint_tool]})
|
|
63
|
+
when "tools/call" then tools_call(id, params, linter)
|
|
64
|
+
else error(id, -32601, "Method not found: #{method}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialize_result(requested_version)
|
|
69
|
+
version = SUPPORTED_VERSIONS.include?(requested_version) ? requested_version : LATEST_VERSION
|
|
70
|
+
{
|
|
71
|
+
"protocolVersion" => version,
|
|
72
|
+
"capabilities" => {"tools" => {}},
|
|
73
|
+
"serverInfo" => {"name" => SERVER_NAME, "version" => SimpleEnglish::VERSION}
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def lint_tool
|
|
78
|
+
{
|
|
79
|
+
"name" => "lint",
|
|
80
|
+
"description" =>
|
|
81
|
+
"Lint Markdown prose and code comments against plain-English rules " \
|
|
82
|
+
"(short sentences, active voice, no jargon, no filler). " \
|
|
83
|
+
"Input: a file path. Returns one line per finding, or 'clean' when " \
|
|
84
|
+
"nothing is flagged. Fix the findings, then call again until clean.",
|
|
85
|
+
"inputSchema" => {
|
|
86
|
+
"type" => "object",
|
|
87
|
+
"properties" => {"path" => {"type" => "string", "description" => "File to lint"}},
|
|
88
|
+
"required" => ["path"]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def tools_call(id, params, linter)
|
|
94
|
+
tool = params["name"]
|
|
95
|
+
return error(id, -32602, "Unknown tool: #{tool}") unless tool == "lint"
|
|
96
|
+
arguments = params["arguments"]
|
|
97
|
+
arguments = {} unless arguments.is_a?(Hash)
|
|
98
|
+
path = arguments["path"]
|
|
99
|
+
return ok(id, tool_error("the path argument is required.")) if path.nil? || path.to_s.empty?
|
|
100
|
+
return ok(id, tool_error("#{path} is a directory. Give one file.")) if File.directory?(path)
|
|
101
|
+
return ok(id, tool_error("file not found: #{path}")) unless File.exist?(path)
|
|
102
|
+
findings =
|
|
103
|
+
begin
|
|
104
|
+
linter.call(path)
|
|
105
|
+
rescue SimpleEnglish::Config::ConfigError => e
|
|
106
|
+
return ok(id, tool_error("invalid .simple-english.yml: #{e.message}"))
|
|
107
|
+
end
|
|
108
|
+
if findings.nil?
|
|
109
|
+
# A tool error, not a protocol error: clients show the text to the
|
|
110
|
+
# model, and this message is the one that tells it how to recover.
|
|
111
|
+
ok(id, tool_error("the se daemon did not answer. Run `se serve --detached` and retry."))
|
|
112
|
+
else
|
|
113
|
+
ok(id, {"content" => [{"type" => "text", "text" => render(path, findings)}], "isError" => false})
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def render(path, findings)
|
|
118
|
+
return "clean" if findings.empty?
|
|
119
|
+
findings.map { |finding| finding.to_line(path) }.join("\n")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def tool_error(text)
|
|
123
|
+
{"content" => [{"type" => "text", "text" => text}], "isError" => true}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def ok(id, result)
|
|
127
|
+
{"jsonrpc" => "2.0", "id" => id, "result" => result}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def error(id, code, message)
|
|
131
|
+
{"jsonrpc" => "2.0", "id" => id, "error" => {"code" => code, "message" => message}}
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# .simple-english.yml: ignore (path globs, matched against the paths
|
|
4
|
+
# given on the command line) and disabled-rules (rule IDs dropped
|
|
5
|
+
# from every file). Loaded from the CWD.
|
|
6
|
+
|
|
7
|
+
module SimpleEnglish
|
|
8
|
+
module Config
|
|
9
|
+
ConfigError = Class.new(StandardError)
|
|
10
|
+
DEFAULT = {ignore: [], disabled_rules: []}.freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def load(dir = Dir.pwd)
|
|
15
|
+
file = File.join(dir, ".simple-english.yml")
|
|
16
|
+
return DEFAULT unless File.exist?(file)
|
|
17
|
+
require "yaml"
|
|
18
|
+
data = YAML.safe_load_file(file) || {}
|
|
19
|
+
{ignore: Array(data["ignore"]),
|
|
20
|
+
disabled_rules: Array(data["disabled-rules"])}
|
|
21
|
+
rescue Psych::SyntaxError => e
|
|
22
|
+
raise ConfigError, ".simple-english.yml: #{e.message}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Hooks pass absolute paths while ignore globs are written
|
|
26
|
+
# relative to the repo root (the CWD). Match both forms.
|
|
27
|
+
# realpath because CWD may be a symlink-resolved path while the
|
|
28
|
+
# caller's string still carries the symlink (TMPDIR on macOS).
|
|
29
|
+
def ignore?(config, path)
|
|
30
|
+
candidates = [path, relative_candidate(path)].compact
|
|
31
|
+
candidates.any? { |candidate| config[:ignore].any? { |pattern| matches?(pattern, candidate) } }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def relative_candidate(path)
|
|
35
|
+
pwd = Dir.pwd + "/"
|
|
36
|
+
resolved = File.exist?(path) ? File.realpath(path) : File.expand_path(path)
|
|
37
|
+
resolved.start_with?(pwd) ? resolved.delete_prefix(pwd) : nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# File.fnmatch has no globstar: `**` never crosses directories.
|
|
41
|
+
# Translate instead: `**` as a whole segment is any depth, `*` and
|
|
42
|
+
# `?` stay within one segment.
|
|
43
|
+
def matches?(pattern, path)
|
|
44
|
+
glob_to_regex(pattern).match?(path)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def glob_to_regex(pattern)
|
|
48
|
+
Regexp.new("\\A" + pattern.split("/").map do |segment|
|
|
49
|
+
if segment == "**"
|
|
50
|
+
"(?:[^/]+/)*[^/]*"
|
|
51
|
+
else
|
|
52
|
+
Regexp.escape(segment).gsub("\\*", "[^/]*").gsub("\\?", "[^/]")
|
|
53
|
+
end
|
|
54
|
+
end.join("/") + "\\z")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def filter(config, path, findings)
|
|
58
|
+
return [] if ignore?(config, path)
|
|
59
|
+
disabled = config[:disabled_rules]
|
|
60
|
+
findings.reject { |finding| disabled.include?(finding.rule) }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Fingerprints for the daemon handshake. `gem` identifies the code and
|
|
4
|
+
# built-in rules a daemon runs: any release that changes lint behavior
|
|
5
|
+
# changes it, and nothing else does. `sha` covers the staged rule
|
|
6
|
+
# content wherever the caller resolved it.
|
|
7
|
+
|
|
8
|
+
require_relative "languagetool"
|
|
9
|
+
|
|
10
|
+
module SimpleEnglish
|
|
11
|
+
module Fingerprint
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Hash everything the daemon can execute, not a curated list: a
|
|
15
|
+
# curated list goes stale the first time a new file is forgotten,
|
|
16
|
+
# and a missed file means silent staleness, the exact bug the
|
|
17
|
+
# handshake exists to catch. The accepted cost: a release that
|
|
18
|
+
# only changes caller-side code triggers one needless restart.
|
|
19
|
+
# ponytail: whole-lib glob. If restarts ever hurt, curate per-file.
|
|
20
|
+
def gem
|
|
21
|
+
files = Dir.glob(File.expand_path("../../**/*.rb", __dir__)).sort
|
|
22
|
+
files << LanguageTool::RULES_FILE
|
|
23
|
+
# Each file is length-prefixed: bare concatenation cannot tell
|
|
24
|
+
# ["ab", "c"] from ["a", "bc"], so bytes moved across a file
|
|
25
|
+
# boundary leave the digest unchanged.
|
|
26
|
+
framed = files.map do |path|
|
|
27
|
+
content = File.read(path)
|
|
28
|
+
"#{content.bytesize}\n#{content}"
|
|
29
|
+
end
|
|
30
|
+
sha(framed.join)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def sha(content)
|
|
34
|
+
require "digest"
|
|
35
|
+
::Digest::SHA256.hexdigest(content)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Resolves the bundled native LanguageTool server once, at the process edge.
|
|
4
|
+
# Tests and source builds can select an explicit executable through the
|
|
5
|
+
# environment. Released platform gems provide the default under libexec.
|
|
6
|
+
|
|
7
|
+
require_relative "languagetool"
|
|
8
|
+
|
|
9
|
+
module SimpleEnglish
|
|
10
|
+
class Install
|
|
11
|
+
class SetupError < StandardError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :executable
|
|
15
|
+
|
|
16
|
+
def initialize(executable:)
|
|
17
|
+
@executable = File.expand_path(executable)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.from_env(env = ENV)
|
|
21
|
+
executable = env.fetch(LanguageTool::EXECUTABLE_ENV,
|
|
22
|
+
LanguageTool::BUNDLED_EXECUTABLE)
|
|
23
|
+
new(executable: executable)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def executable?
|
|
27
|
+
File.file?(executable) && File.executable?(executable)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def executable!
|
|
31
|
+
return executable if executable?
|
|
32
|
+
raise SetupError, setup_error
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def setup_error
|
|
36
|
+
"lint engine not found or not executable at #{executable}. " \
|
|
37
|
+
"Install a supported platform build of this gem."
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Pinned LanguageTool facts shared by the native build and Ruby runtime.
|
|
4
|
+
|
|
5
|
+
module SimpleEnglish
|
|
6
|
+
module LanguageTool
|
|
7
|
+
RULES_FILE = File.expand_path("../../../rules/simple-english.xml", __dir__)
|
|
8
|
+
LT_VERSION = "6.6"
|
|
9
|
+
EXECUTABLE_ENV = "SE_LANGUAGETOOL_EXECUTABLE"
|
|
10
|
+
BUNDLED_EXECUTABLE = File.expand_path(
|
|
11
|
+
"../../../libexec/simple_english/languagetool-server", __dir__
|
|
12
|
+
)
|
|
13
|
+
TIMEOUT_SECONDS = 30
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def rule_ids(path = RULES_FILE)
|
|
18
|
+
File.read(path, encoding: "UTF-8").scan(/<(?:rule|rulegroup)\b[^>]*>/).filter_map do |tag|
|
|
19
|
+
tag[/\bid="([^"]+)"/, 1]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Lint Markdown prose with the SimpleEnglish Plain-mode rules.
|
|
4
|
+
# Pattern rules run on LanguageTool. Counting rules run here.
|
|
5
|
+
# This file is the composition root. The pieces live in lib/simple_english/.
|
|
6
|
+
|
|
7
|
+
require_relative "simple_english/version"
|
|
8
|
+
require_relative "simple_english/lint/finding"
|
|
9
|
+
require_relative "simple_english/lint/paragraph"
|
|
10
|
+
require_relative "simple_english/lint/markdown"
|
|
11
|
+
require_relative "simple_english/lint/counts"
|
|
12
|
+
require_relative "simple_english/setup/languagetool"
|
|
13
|
+
require_relative "simple_english/setup/install"
|
|
14
|
+
require_relative "simple_english/lint/extractor"
|
|
15
|
+
require_relative "simple_english/lint/lint_plan"
|
|
16
|
+
require_relative "simple_english/lint/annotated_text"
|
|
17
|
+
require_relative "simple_english/lint/suppressions"
|
|
18
|
+
require_relative "simple_english/setup/config"
|
|
19
|
+
require_relative "simple_english/setup/fingerprint"
|
|
20
|
+
require_relative "simple_english/client/language_tool"
|
|
21
|
+
require_relative "simple_english/client/daemon"
|
|
22
|
+
require_relative "simple_english/daemon/engine"
|
|
23
|
+
require_relative "simple_english/daemon/http"
|
|
24
|
+
require_relative "simple_english/model_context_protocol"
|
|
25
|
+
require_relative "simple_english/daemon/server"
|
|
26
|
+
|
|
27
|
+
module SimpleEnglish
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# Returns findings, or nil when the daemon is unreachable. The
|
|
31
|
+
# caller (bin/se) owns the exit status. Diagnostics go to
|
|
32
|
+
# stderr here.
|
|
33
|
+
def lint_text(text)
|
|
34
|
+
return nil unless Client.ensure_up(install: Install.from_env)
|
|
35
|
+
Client.lint(text)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# One entry point for files. LintPlan decides what kind of lint
|
|
39
|
+
# this is. Both tiers share it, so they cannot drift. Code files
|
|
40
|
+
# lint comments through the daemon. nil means unreachable:
|
|
41
|
+
# the caller decides how fatal that is.
|
|
42
|
+
def lint_file(path, text = File.read(path))
|
|
43
|
+
language = Extractor.language_for(path)
|
|
44
|
+
plan = LintPlan.call(text, language)
|
|
45
|
+
return [] if plan.nil?
|
|
46
|
+
findings =
|
|
47
|
+
if plan == :prose
|
|
48
|
+
lint_text(text)
|
|
49
|
+
else
|
|
50
|
+
return nil unless Client.ensure_up(install: Install.from_env)
|
|
51
|
+
result = Client.lint(text, language: language)
|
|
52
|
+
if result.nil?
|
|
53
|
+
warn "error: se daemon did not answer. Run `#{Client.runner} serve` and read its output."
|
|
54
|
+
end
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
return nil unless findings
|
|
58
|
+
Suppressions.filter(text, findings)
|
|
59
|
+
end
|
|
60
|
+
end
|