writing-check-cli 1.0.0__py3-none-any.whl

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.
Files changed (48) hide show
  1. provenance.json +1 -0
  2. writing_check/__init__.py +1 -0
  3. writing_check/cli.py +268 -0
  4. writing_check/sweep.py +120 -0
  5. writing_check/vale/.vale.ini +4 -0
  6. writing_check/vale/profiles.json +489 -0
  7. writing_check/vale/styles/GoogleDocs/AMPM.yml +9 -0
  8. writing_check/vale/styles/GoogleDocs/Acronyms.yml +64 -0
  9. writing_check/vale/styles/GoogleDocs/Anthropomorphism.yml +12 -0
  10. writing_check/vale/styles/GoogleDocs/Colons.yml +13 -0
  11. writing_check/vale/styles/GoogleDocs/Contractions.yml +30 -0
  12. writing_check/vale/styles/GoogleDocs/DateFormat.yml +9 -0
  13. writing_check/vale/styles/GoogleDocs/Ellipses.yml +9 -0
  14. writing_check/vale/styles/GoogleDocs/EmDash.yml +13 -0
  15. writing_check/vale/styles/GoogleDocs/ExcessiveClaims.yml +14 -0
  16. writing_check/vale/styles/GoogleDocs/Exclamation.yml +12 -0
  17. writing_check/vale/styles/GoogleDocs/FirstPerson.yml +15 -0
  18. writing_check/vale/styles/GoogleDocs/Gender.yml +9 -0
  19. writing_check/vale/styles/GoogleDocs/GenderBias.yml +43 -0
  20. writing_check/vale/styles/GoogleDocs/HeadingPunctuation.yml +13 -0
  21. writing_check/vale/styles/GoogleDocs/Headings.yml +32 -0
  22. writing_check/vale/styles/GoogleDocs/Jargon.yml +13 -0
  23. writing_check/vale/styles/GoogleDocs/Latin.yml +15 -0
  24. writing_check/vale/styles/GoogleDocs/LyHyphens.yml +14 -0
  25. writing_check/vale/styles/GoogleDocs/OptionalPlurals.yml +12 -0
  26. writing_check/vale/styles/GoogleDocs/Ordinal.yml +7 -0
  27. writing_check/vale/styles/GoogleDocs/OxfordComma.yml +28 -0
  28. writing_check/vale/styles/GoogleDocs/Parens.yml +15 -0
  29. writing_check/vale/styles/GoogleDocs/Passive.yml +184 -0
  30. writing_check/vale/styles/GoogleDocs/Periods.yml +7 -0
  31. writing_check/vale/styles/GoogleDocs/Quotes.yml +7 -0
  32. writing_check/vale/styles/GoogleDocs/Ranges.yml +7 -0
  33. writing_check/vale/styles/GoogleDocs/Semicolons.yml +8 -0
  34. writing_check/vale/styles/GoogleDocs/Slang.yml +11 -0
  35. writing_check/vale/styles/GoogleDocs/Spacing.yml +10 -0
  36. writing_check/vale/styles/GoogleDocs/Spelling.yml +10 -0
  37. writing_check/vale/styles/GoogleDocs/Timeless.yml +13 -0
  38. writing_check/vale/styles/GoogleDocs/Units.yml +10 -0
  39. writing_check/vale/styles/GoogleDocs/We.yml +11 -0
  40. writing_check/vale/styles/GoogleDocs/Will.yml +7 -0
  41. writing_check/vale/upstream-lock.json +19 -0
  42. writing_check/vale/vendor/Google-LICENSE +21 -0
  43. writing_check/vale/vendor/Google-NOTICE.md +7 -0
  44. writing_check_cli-1.0.0.dist-info/METADATA +34 -0
  45. writing_check_cli-1.0.0.dist-info/RECORD +48 -0
  46. writing_check_cli-1.0.0.dist-info/WHEEL +4 -0
  47. writing_check_cli-1.0.0.dist-info/entry_points.txt +3 -0
  48. writing_check_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
provenance.json ADDED
@@ -0,0 +1 @@
1
+ {"schema": 1, "decision": "This package contains the independently authored checker and reviewed Google Vale v0.7.1 policy data.", "entries": [{"path_prefix": "src/writing_check/", "origin": "same-author portable implementation; vale policy includes reviewed upstream Google rules", "license": "MIT"}, {"path": "src/writing_check/vale/vendor/Google-NOTICE.md", "origin": "same-author attribution of vale-cli/Google v0.7.1 and its source guide; see upstream-lock.json", "license": "MIT"}, {"path": "LICENSE", "origin": "standard MIT text", "license": "MIT"}, {"path": ".gitignore", "origin": "shared repository build exclusions", "license": "MIT"}, {"path": "README.md", "origin": "same-author package documentation", "license": "MIT"}, {"path": "pyproject.toml", "origin": "same-author package build and project metadata", "license": "MIT"}, {"path": "provenance.json", "origin": "package-scoped provenance inventory", "license": "MIT"}]}
@@ -0,0 +1 @@
1
+ """Portable source-bound Writing Check package."""
writing_check/cli.py ADDED
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env python3
2
+ """Read-only source-bound advisory writing evidence with optional Vale JSON."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import pathlib
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ from typing import Any
15
+
16
+ EXIT_CLEAN, EXIT_FINDINGS, EXIT_INPUT, EXIT_UNAVAILABLE, EXIT_EXECUTION = range(5)
17
+ ROOT = pathlib.Path(__file__).resolve().parent
18
+ WORD = re.compile(r"[A-Za-z]+(?:['’][A-Za-z]+)?")
19
+ SENTENCE = re.compile(r"[.!?]+")
20
+
21
+
22
+ class PolicyError(ValueError):
23
+ """Raised when the bundled public policy closure is not intact."""
24
+
25
+
26
+ class UnknownProfile(ValueError):
27
+ """Raised for the existing public input-error profile boundary."""
28
+
29
+ def __init__(self, profiles: list[str]) -> None:
30
+ self.profiles = profiles
31
+
32
+
33
+ def policy_file(root: pathlib.Path, relative: str) -> pathlib.Path:
34
+ path = (root / relative).resolve()
35
+ if root not in path.parents or not path.is_file():
36
+ raise PolicyError(f"missing policy file: {relative}")
37
+ return path
38
+
39
+
40
+ def load_policy(profile: str) -> tuple[dict[str, Any], str]:
41
+ vale = ROOT / "vale"
42
+ try:
43
+ manifest_bytes = policy_file(vale, "profiles.json").read_bytes()
44
+ lock_bytes = policy_file(vale, "upstream-lock.json").read_bytes()
45
+ manifest = json.loads(manifest_bytes)
46
+ lock = json.loads(lock_bytes)
47
+ if not isinstance(lock, dict):
48
+ raise PolicyError("policy lock has an invalid root")
49
+ except (OSError, json.JSONDecodeError, PolicyError) as error:
50
+ raise PolicyError(f"invalid policy metadata: {error}") from error
51
+ profiles, rules, config = manifest.get("profiles"), manifest.get("rules"), manifest.get("config")
52
+ if not isinstance(profiles, dict) or not isinstance(rules, dict):
53
+ raise PolicyError("policy manifest has an invalid profile or rule inventory")
54
+ if not isinstance(config, dict) or config.get("path") != ".vale.ini" or not isinstance(config.get("sha256"), str):
55
+ raise PolicyError("invalid policy config metadata")
56
+ config_bytes = policy_file(vale, config["path"]).read_bytes()
57
+ if hashlib.sha256(config_bytes).hexdigest() != config["sha256"]:
58
+ raise PolicyError("policy config digest mismatch")
59
+ if profile not in profiles:
60
+ raise UnknownProfile(sorted(profiles))
61
+ selected = profiles[profile]
62
+ if not isinstance(selected, dict) or not isinstance(selected.get("policy"), str):
63
+ raise PolicyError(f"invalid policy profile: {profile}")
64
+ upstream = lock.get("upstream")
65
+ if not isinstance(upstream, dict) or not isinstance(upstream.get("license_file"), str) or not isinstance(upstream.get("license_sha256"), str):
66
+ raise PolicyError("invalid policy license metadata")
67
+ license_path = policy_file(vale, upstream["license_file"])
68
+ license_bytes = license_path.read_bytes()
69
+ if hashlib.sha256(license_bytes).hexdigest() != upstream["license_sha256"]:
70
+ raise PolicyError("policy license digest mismatch")
71
+ declared: set[str] = set()
72
+ selected_files: list[tuple[str, bytes]] = []
73
+ for name, metadata in sorted(rules.items()):
74
+ if not isinstance(name, str) or not isinstance(metadata, dict):
75
+ raise PolicyError("invalid policy rule metadata")
76
+ relative, digest, rule_profiles = metadata.get("path"), metadata.get("sha256"), metadata.get("profiles")
77
+ if not isinstance(relative, str) or not isinstance(digest, str) or not isinstance(rule_profiles, list):
78
+ raise PolicyError(f"invalid policy rule metadata: {name}")
79
+ if profile not in rule_profiles or not all(isinstance(item, str) for item in rule_profiles):
80
+ raise PolicyError(f"invalid policy rule profile closure: {name}")
81
+ path = policy_file(vale, relative)
82
+ contents = path.read_bytes()
83
+ if hashlib.sha256(contents).hexdigest() != digest:
84
+ raise PolicyError(f"policy rule digest mismatch: {relative}")
85
+ declared.add(relative)
86
+ selected_files.append((relative, contents))
87
+ actual = {path.relative_to(vale).as_posix() for path in (vale / "styles" / "GoogleDocs").glob("*.yml")}
88
+ if not declared or actual != declared:
89
+ raise PolicyError("policy rule inventory mismatch")
90
+ identity = [(".vale.ini", config_bytes), ("profiles.json", manifest_bytes), ("upstream-lock.json", lock_bytes), (upstream["license_file"], license_bytes), *selected_files]
91
+ digest = hashlib.sha256()
92
+ for relative, contents in identity:
93
+ digest.update(relative.encode("utf-8")); digest.update(b"\0"); digest.update(contents); digest.update(b"\0")
94
+ return selected, digest.hexdigest()
95
+
96
+
97
+ def source(path: str) -> tuple[str, str, bytes]:
98
+ if path == "-":
99
+ return "stdin", "stdin", sys.stdin.buffer.read()
100
+ file = pathlib.Path(path).expanduser().resolve()
101
+ if not file.is_file():
102
+ raise OSError(f"input is not a regular file: {file}")
103
+ return "file", str(file), file.read_bytes()
104
+
105
+
106
+ def prose_parts(text: str) -> tuple[str, str]:
107
+ included, excluded = [], []
108
+ fence = False
109
+ for line in text.splitlines():
110
+ if re.match(r"^\s*(```|~~~)", line):
111
+ fence = not fence
112
+ continue
113
+ cleaned = re.sub(r"`[^`]*`|https?://\S+|!\[[^]]*\]\([^)]*\)", " ", line)
114
+ cleaned = re.sub(r"\[([^]]+)\]\([^)]*\)", r"\1", cleaned)
115
+ if fence or line.startswith("---") or re.match(r"^\s*(#|<!--|<[^>]+>)", line):
116
+ excluded.append(cleaned)
117
+ elif "|" in line or re.match(r"^\s*[-*+]\s+\S{1,4}\s*$", line):
118
+ excluded.append(cleaned)
119
+ else:
120
+ included.append(cleaned)
121
+ return " ".join(included), " ".join(excluded)
122
+
123
+
124
+ def readability(text: str, profile: str) -> dict[str, Any]:
125
+ included, excluded = prose_parts(text)
126
+ words, excluded_words = WORD.findall(included), WORD.findall(excluded)
127
+ sentences = len(SENTENCE.findall(included))
128
+ if profile == "commit":
129
+ return {"status": "disabled", "words": len(words), "sentences": sentences, "targets": None}
130
+ if profile in {"readme", "technical-doc-reference"} and len(excluded_words) > len(words):
131
+ return {"status": "composition_limited", "words": len(words), "sentences": sentences, "excluded_words": len(excluded_words), "targets": None}
132
+ minimum_words, minimum_sentences = (200, 8) if profile in {"pr-description", "user-facing-response"} else (300, 8)
133
+ state = "insufficient_sample" if len(words) < 100 or sentences < 5 else "small_sample" if len(words) < minimum_words or sentences < minimum_sentences else "normal"
134
+ result: dict[str, Any] = {"status": state, "words": len(words), "sentences": sentences, "targets": {"fre_min": 55, "fk_max": 10} if state == "normal" else None}
135
+ if state == "normal":
136
+ syllables = sum(max(1, len(re.findall(r"[aeiouy]+", word.lower()))) for word in words)
137
+ result["flesch_reading_ease"] = round(206.835 - 1.015 * len(words) / sentences - 84.6 * syllables / len(words), 2)
138
+ result["flesch_kincaid_grade"] = round(.39 * len(words) / sentences + 11.8 * syllables / len(words) - 15.59, 2)
139
+ return result
140
+
141
+
142
+ def normalize_vale(raw: str) -> list[dict[str, Any]]:
143
+ try:
144
+ document = json.loads(raw)
145
+ except json.JSONDecodeError as error:
146
+ raise ValueError(f"invalid Vale JSON: {error}") from error
147
+ if not isinstance(document, dict):
148
+ raise ValueError("invalid Vale JSON root: expected an object")
149
+ alerts: list[dict[str, Any]] = []
150
+ for path, items in sorted(document.items()):
151
+ if not isinstance(items, list):
152
+ raise ValueError(f"invalid Vale alerts for {path}: expected a list")
153
+ for item in items:
154
+ if not isinstance(item, dict):
155
+ raise ValueError(f"invalid Vale alert for {path}: expected an object")
156
+ check, message, line, span = item.get("Check"), item.get("Message"), item.get("Line"), item.get("Span")
157
+ if not isinstance(check, str) or not check or not isinstance(message, str) or not message:
158
+ raise ValueError(f"invalid Vale alert fields for {path}")
159
+ if isinstance(line, bool) or not isinstance(line, int) or line < 1:
160
+ raise ValueError(f"invalid Vale alert line for {path}")
161
+ if not isinstance(span, list) or len(span) != 2 or any(isinstance(value, bool) or not isinstance(value, int) for value in span) or span[0] < 1 or span[1] < span[0]:
162
+ raise ValueError(f"invalid Vale alert span for {path}")
163
+ alerts.append({"source": "vale", "rule": check, "message": message, "severity": "suggestion", "line": line, "span": span, "path": path})
164
+ return alerts
165
+
166
+
167
+ def print_record(record: dict[str, Any]) -> None:
168
+ print(json.dumps(record, indent=2))
169
+
170
+
171
+ def resolve_vale() -> str | None:
172
+ """Prefer Vale installed beside this command's Python interpreter."""
173
+ environment_vale = pathlib.Path(sys.executable).parent / "vale"
174
+ if environment_vale.is_file() and os.access(environment_vale, os.X_OK):
175
+ return str(environment_vale)
176
+ return shutil.which("vale")
177
+
178
+
179
+ def main() -> int:
180
+ parser = argparse.ArgumentParser()
181
+ parser.add_argument("--profile", required=True)
182
+ parser.add_argument("--json", action="store_true")
183
+ parser.add_argument("input")
184
+ args = parser.parse_args()
185
+ try:
186
+ selected_policy, policy_digest = load_policy(args.profile)
187
+ except UnknownProfile as error:
188
+ print_record({"schema": 3, "status": "input_error", "error": {"code": "unknown_profile", "message": f"unknown profile: {args.profile}"}, "allowed_profiles": error.profiles})
189
+ return EXIT_INPUT
190
+ except PolicyError as error:
191
+ print_record({"schema": 3, "status": "execution_error", "error": {"code": "policy_validation_failed", "message": str(error)}})
192
+ return EXIT_EXECUTION
193
+ try:
194
+ kind, identifier, data = source(args.input)
195
+ text = data.decode("utf-8")
196
+ except (OSError, UnicodeDecodeError) as error:
197
+ print_record({"schema": 3, "status": "input_error", "profile": args.profile, "error": {"code": "invalid_utf8" if isinstance(error, UnicodeDecodeError) else "input_read_failed", "message": str(error)}})
198
+ return EXIT_INPUT
199
+ record: dict[str, Any] = {"schema": 3, "status": None, "advisory": True, "profile": args.profile, "policy": {"name": selected_policy["policy"], "sha256": policy_digest}, "source": {"kind": kind, "identifier": identifier, "encoding": "utf-8", "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest(), "text": text}, "readability": readability(text, args.profile), "alerts": [], "vale": {}}
200
+ vale = resolve_vale()
201
+ if not vale:
202
+ record.update(status="unavailable", vale={"status": "unavailable", "reason": "Vale was not found; no substitute was run."})
203
+ print_record(record)
204
+ return EXIT_UNAVAILABLE
205
+ version_argv = [vale, "--version"]
206
+ try:
207
+ version_run = subprocess.run(version_argv, capture_output=True, text=False, timeout=30)
208
+ except (OSError, subprocess.TimeoutExpired) as error:
209
+ version: dict[str, Any] = {"argv": version_argv}
210
+ if isinstance(error, subprocess.TimeoutExpired):
211
+ if error.stdout is not None:
212
+ version["stdout"] = error.stdout.decode("utf-8", errors="replace")
213
+ if error.stderr is not None:
214
+ version["stderr"] = error.stderr.decode("utf-8", errors="replace")
215
+ record.update(status="execution_error", vale={"status": "execution_error", "version": version, "error": str(error)})
216
+ print_record(record)
217
+ return EXIT_EXECUTION
218
+ try:
219
+ version_stdout, version_stderr = version_run.stdout.decode("utf-8"), version_run.stderr.decode("utf-8")
220
+ except UnicodeDecodeError as error:
221
+ version = {"argv": version_argv, "exit": version_run.returncode, "stdout": version_run.stdout.decode("utf-8", errors="replace"), "stderr": version_run.stderr.decode("utf-8", errors="replace")}
222
+ record.update(status="execution_error", vale={"status": "execution_error", "version": version, "error": f"invalid UTF-8 in Vale version output: {error}"})
223
+ print_record(record)
224
+ return EXIT_EXECUTION
225
+ version = {"argv": version_argv, "exit": version_run.returncode, "stdout": version_stdout, "stderr": version_stderr}
226
+ if version_run.returncode != 0:
227
+ record.update(status="execution_error", vale={"status": "execution_error", "version": version, "error": f"Vale version exited {version_run.returncode}"})
228
+ print_record(record)
229
+ return EXIT_EXECUTION
230
+ lint_argv = [vale, "--no-global", f"--config={ROOT / 'vale' / '.vale.ini'}", "--no-exit", "--output=JSON", "--ext=.md", "-"]
231
+ lint: dict[str, Any] = {"argv": lint_argv}
232
+ record["vale"] = {"status": "ran", "version": version, "lint": lint}
233
+ try:
234
+ lint_run = subprocess.run(lint_argv, input=data, capture_output=True, text=False, timeout=30)
235
+ try:
236
+ stdout, stderr = lint_run.stdout.decode("utf-8"), lint_run.stderr.decode("utf-8")
237
+ except UnicodeDecodeError as error:
238
+ lint = {"argv": lint_argv, "exit": lint_run.returncode, "stdout": lint_run.stdout.decode("utf-8", errors="replace"), "stderr": lint_run.stderr.decode("utf-8", errors="replace")}
239
+ record["vale"] = {"status": "ran", "version": version, "lint": lint, **lint}
240
+ raise ValueError(f"invalid UTF-8 in Vale output: {error}") from error
241
+ lint = {"argv": lint_argv, "exit": lint_run.returncode, "stdout": stdout, "stderr": stderr}
242
+ record["vale"] = {"status": "ran", "version": version, "lint": lint, **lint}
243
+ if lint_run.returncode not in (0, 1):
244
+ raise RuntimeError(f"Vale exited {lint_run.returncode}")
245
+ record["alerts"] = normalize_vale(stdout)
246
+ except (OSError, UnicodeDecodeError, subprocess.TimeoutExpired, ValueError, RuntimeError) as error:
247
+ if isinstance(error, subprocess.TimeoutExpired):
248
+ if error.stdout is not None:
249
+ lint["stdout"] = error.stdout.decode("utf-8", errors="replace")
250
+ if error.stderr is not None:
251
+ lint["stderr"] = error.stderr.decode("utf-8", errors="replace")
252
+ vale_record = record.get("vale", {})
253
+ vale_record.update(status="execution_error", version=version, error=str(error))
254
+ record.update(status="execution_error", vale=vale_record)
255
+ print_record(record)
256
+ return EXIT_EXECUTION
257
+ record["status"] = "findings" if record["alerts"] else "no_findings"
258
+ print_record(record)
259
+ return EXIT_FINDINGS if record["alerts"] else EXIT_CLEAN
260
+
261
+
262
+ def entrypoint() -> None:
263
+ """Run the installed Writing Check command."""
264
+ raise SystemExit(main())
265
+
266
+
267
+ if __name__ == "__main__":
268
+ entrypoint()
writing_check/sweep.py ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """Run explicitly supplied Writing Check candidates in one Python process.
3
+
4
+ This caller intentionally does not discover repository files or alter the
5
+ single-candidate checker contract. It emits one unchanged schema-3 JSON record
6
+ per input candidate (JSON Lines).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import contextlib
12
+ import importlib
13
+ import io
14
+ import json
15
+ import pathlib
16
+ import subprocess
17
+ import sys
18
+ from typing import Any, Callable, Protocol, TypeAlias, cast
19
+
20
+ CHECKER_PATH = pathlib.Path(__file__).with_name("cli.py")
21
+ ProbeFailure: TypeAlias = OSError | subprocess.TimeoutExpired
22
+ ProbeResult: TypeAlias = subprocess.CompletedProcess[bytes] | ProbeFailure
23
+
24
+
25
+ class _SubprocessModule(Protocol):
26
+ run: Callable[..., subprocess.CompletedProcess[bytes]]
27
+
28
+
29
+ class _Checker(Protocol):
30
+ subprocess: _SubprocessModule
31
+
32
+ def main(self) -> int: ...
33
+
34
+
35
+ def _load_checker() -> _Checker:
36
+ return cast(_Checker, importlib.import_module("writing_check.cli"))
37
+
38
+
39
+ def _input_error(message: str) -> dict[str, Any]:
40
+ return {
41
+ "schema": 3,
42
+ "status": "input_error",
43
+ "error": {"code": "invalid_candidates", "message": message},
44
+ }
45
+
46
+
47
+ def _candidates(path: pathlib.Path) -> list[tuple[str, str]]:
48
+ try:
49
+ data = json.loads(path.read_text(encoding="utf-8"))
50
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
51
+ raise ValueError(f"cannot read candidates: {error}") from error
52
+ if not isinstance(data, list) or not data:
53
+ raise ValueError("candidates must be a non-empty JSON array")
54
+ result: list[tuple[str, str]] = []
55
+ for index, item in enumerate(data):
56
+ if not isinstance(item, dict) or set(item) != {"source", "profile"}:
57
+ raise ValueError(f"candidate {index} must contain exactly source and profile")
58
+ source, profile = item["source"], item["profile"]
59
+ if not isinstance(source, str) or not source or source == "-" or not isinstance(profile, str):
60
+ raise ValueError(f"candidate {index} has an invalid source or profile")
61
+ result.append((source, profile))
62
+ return result
63
+
64
+
65
+ def run(candidates: list[tuple[str, str]]) -> tuple[list[dict[str, Any]], int]:
66
+ """Return independent checker records while sharing only Vale version probing."""
67
+ checker = _load_checker()
68
+ original_run = checker.subprocess.run
69
+ cached_versions: dict[tuple[str, ...], ProbeResult] = {}
70
+
71
+ def cached_run(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[bytes]:
72
+ if argv[1:] != ["--version"]:
73
+ return original_run(argv, **kwargs)
74
+ key = tuple(argv)
75
+ if key not in cached_versions:
76
+ try:
77
+ cached_versions[key] = original_run(argv, **kwargs)
78
+ except (OSError, subprocess.TimeoutExpired) as error: # checker serializes its own probe failure
79
+ cached_versions[key] = error
80
+ cached_version = cached_versions[key]
81
+ if isinstance(cached_version, (OSError, subprocess.TimeoutExpired)):
82
+ raise cached_version
83
+ return cached_version
84
+
85
+ checker.subprocess.run = cached_run
86
+ records: list[dict[str, Any]] = []
87
+ exit_code = 0
88
+ old_argv = sys.argv
89
+ try:
90
+ for source, profile in candidates:
91
+ output = io.StringIO()
92
+ sys.argv = [str(CHECKER_PATH), f"--profile={profile}", "--json", "--", source]
93
+ with contextlib.redirect_stdout(output):
94
+ code = checker.main()
95
+ records.append(json.loads(output.getvalue()))
96
+ exit_code = max(exit_code, code)
97
+ finally:
98
+ sys.argv = old_argv
99
+ checker.subprocess.run = original_run
100
+ return records, exit_code
101
+
102
+
103
+ def main() -> int:
104
+ parser = argparse.ArgumentParser(description="bounded explicit Writing Check sweep")
105
+ parser.add_argument("--candidates", required=True, metavar="JSON", help="JSON array of exact {source, profile} pairs")
106
+ parser.add_argument("--jsonl", action="store_true", required=True, help="emit one schema-3 record per line")
107
+ args = parser.parse_args()
108
+ try:
109
+ candidates = _candidates(pathlib.Path(args.candidates))
110
+ except ValueError as error:
111
+ print(json.dumps(_input_error(str(error))))
112
+ return 2
113
+ records, code = run(candidates)
114
+ for record in records:
115
+ print(json.dumps(record))
116
+ return code
117
+
118
+
119
+ if __name__ == "__main__":
120
+ raise SystemExit(main())
@@ -0,0 +1,4 @@
1
+ StylesPath = styles
2
+ MinAlertLevel = suggestion
3
+ [*.md]
4
+ BasedOnStyles = GoogleDocs