bigpowers 2.86.8 → 2.87.1
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.
- package/.agents/skills/simple-english/SKILL.md +346 -0
- package/.cline/skills/simple-english/SKILL.md +346 -0
- package/.codebuddy/skills/simple-english/SKILL.md +346 -0
- package/.codex/skills/simple-english/SKILL.md +346 -0
- package/.copilot/skills/simple-english/SKILL.md +346 -0
- package/.hermes/skills/simple-english/SKILL.md +346 -0
- package/.kilocode/rules/simple-english.md +346 -0
- package/.mimocode/skills/simple-english/SKILL.md +346 -0
- package/.opencode/skills/simple-english/SKILL.md +346 -0
- package/.pi/package.json +2 -2
- package/.pi/prompts/simple-english.md +345 -0
- package/.pi/skills/simple-english/SKILL.md +346 -0
- package/.qwen/skills/simple-english/SKILL.md +346 -0
- package/.trae/skills/simple-english/SKILL.md +346 -0
- package/.windsurf/rules/simple-english.md +346 -0
- package/.zcode/skills/simple-english/SKILL.md +346 -0
- package/CHANGELOG.md +14 -0
- package/CONVENTIONS.md +1 -0
- package/NOTICES.md +62 -0
- package/README.md +11 -8
- package/SKILL-INDEX.md +11 -10
- package/bin/bigpowers.js +46 -3
- package/package.json +1 -1
- package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_links.cpython-312.pyc +0 -0
- package/skills/simple-english/REFERENCE.md +275 -0
- package/skills/simple-english/SKILL.md +71 -0
- package/skills/simple-english/scripts/ste_lint.py +446 -0
- package/skills-lock.json +5 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Machine gate for ASD-STE100 Simplified Technical English.
|
|
3
|
+
|
|
4
|
+
A deterministic regex pass over technical prose. It counts the violations a
|
|
5
|
+
machine can catch: sentence length, contractions, banned modals, perfect
|
|
6
|
+
tenses, "-ing" clauses, semicolons, Latin abbreviations, slop words, trailing
|
|
7
|
+
conditions, synonym rotation, and terminology drift. It adds two structural
|
|
8
|
+
checks the upstream STE skills omit: heading hierarchy and non-descriptive link
|
|
9
|
+
text. When Vale is on PATH and a `.vale.ini` lives up the tree, it defers the
|
|
10
|
+
style layer to Vale and keeps only the STE checks.
|
|
11
|
+
|
|
12
|
+
Ceiling: this is a regex pass, not a grammar parser. It undercounts (no
|
|
13
|
+
passive-voice detection, no part-of-speech checks) and it can misread sentence
|
|
14
|
+
bounds in unusual markdown. Counts are comparable between two texts run through
|
|
15
|
+
the same version. They are not a compliance verdict. No tool can guarantee STE
|
|
16
|
+
compliance.
|
|
17
|
+
|
|
18
|
+
Synthesis of two MIT sources:
|
|
19
|
+
- ste_lint.py AminBlg/SimpleEnglish (11 STE checks, JSON mode)
|
|
20
|
+
- check_docs.py JuanMarchetto/doc-standards (drift detector, Vale handoff)
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
python3 ste_lint.py --self-test
|
|
24
|
+
python3 ste_lint.py file.md # lint one file (exit 1 on error)
|
|
25
|
+
python3 ste_lint.py path/ # lint a tree of markdown
|
|
26
|
+
python3 ste_lint.py file.md --json # JSON report instead of line output
|
|
27
|
+
python3 ste_lint.py file.md --type procedural # procedural limit (20); default descriptive (25)
|
|
28
|
+
cat text.md | python3 ste_lint.py - # read from stdin
|
|
29
|
+
python3 ste_lint.py file.md --no-vale # force built-in style checks
|
|
30
|
+
|
|
31
|
+
Exit code 1 on any error-level finding, else 0. Warnings never fail the gate.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import json
|
|
37
|
+
import re
|
|
38
|
+
import shutil
|
|
39
|
+
import subprocess
|
|
40
|
+
import sys
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import List, Tuple
|
|
43
|
+
|
|
44
|
+
# --------------------------------------------------------------------------
|
|
45
|
+
# Rule constants (paraphrased from ASD-STE100 Issue 9, 2025-01-15)
|
|
46
|
+
# --------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
LIMITS = {"procedural": 20, "descriptive": 25}
|
|
49
|
+
DEFAULT_TYPE = "descriptive"
|
|
50
|
+
LONG_SENTENCE_ERROR = 40 # hard error regardless of mode (prose, not a step)
|
|
51
|
+
|
|
52
|
+
# Approved modals: can, will, must. Banned: should, would, may, might, could.
|
|
53
|
+
BANNED_MODALS = re.compile(r"\b(should|would|may|might|could)\b", re.I)
|
|
54
|
+
PERFECT = re.compile(
|
|
55
|
+
r"\b(has|have|had)\s+been\b|\b(has|have)\s+\w+ed\b", re.I
|
|
56
|
+
)
|
|
57
|
+
PROGRESSIVE_PASSIVE = re.compile(
|
|
58
|
+
r"\b(?:is|are|was|were)\s+being\s+\w+(?:ed|en)?\b", re.I
|
|
59
|
+
)
|
|
60
|
+
CONTRACTION = re.compile(
|
|
61
|
+
r"\b\w+(n't|'ll|'re|'ve|'d)\b|\bit's\b|\byou're\b|\bdon't\b", re.I
|
|
62
|
+
)
|
|
63
|
+
# "-ing" clause used as a verb (Rule 3.5): ", making ...", ", allowing ..."
|
|
64
|
+
ING_CLAUSE = re.compile(
|
|
65
|
+
r",\s*(mak|allow|enabl|ensur|highlight|creat|provid|offer|help|reduc|"
|
|
66
|
+
r"improv|lead|caus|result)ing\b",
|
|
67
|
+
re.I,
|
|
68
|
+
)
|
|
69
|
+
LATIN = re.compile(r"\b(e\.g\.|i\.e\.|etc\.?)(?=[\s,);]|$)", re.I)
|
|
70
|
+
SLOP = re.compile(
|
|
71
|
+
r"\b(simply|seamlessly|effortlessly|robust|leverag\w*|utiliz\w*|"
|
|
72
|
+
r"comprehensive|powerful|blazingly|streamlin\w*|facilitat\w*|"
|
|
73
|
+
r"performant|plethora|myriad|delve|crucial|pivotal)\b",
|
|
74
|
+
re.I,
|
|
75
|
+
)
|
|
76
|
+
# Condition at the wrong end of a sentence (Rule 5.4): "... if X."
|
|
77
|
+
TRAILING_COND = re.compile(r"\w[^.!?\n]{3,}\s\b(if|when)\b\s", re.I)
|
|
78
|
+
|
|
79
|
+
# Positional references break under retrieval: "as mentioned above".
|
|
80
|
+
POSITIONAL = re.compile(
|
|
81
|
+
r"\b(?:as\s+(?:mentioned|shown|described|noted)\s+)?(?:above|below)\b"
|
|
82
|
+
r"(?=[,.\s)])",
|
|
83
|
+
re.I,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Terminology drift: 2+ synonyms each used 2+ times = drift.
|
|
87
|
+
SYNONYM_SETS: List[List[str]] = [
|
|
88
|
+
["verify", "check", "confirm", "ensure", "make sure", "validate"],
|
|
89
|
+
["cancel", "abort", "terminate"],
|
|
90
|
+
["remove", "delete", "erase"],
|
|
91
|
+
["display", "show", "render"],
|
|
92
|
+
["directory", "folder"],
|
|
93
|
+
["parameter", "argument", "flag", "option"],
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
# Markdown atoms.
|
|
97
|
+
MD_LINK = re.compile(r"\[([^\]]+)\]\(([^)#\s]+)(?:#[^)\s]*)?\)")
|
|
98
|
+
HEADING = re.compile(r"^(#{1,6})\s+(.*)$")
|
|
99
|
+
CODE_FENCE = re.compile(r"^(```|~~~)")
|
|
100
|
+
STEP_ITEM = re.compile(r"^\s*(?:\d+[.)]|[-*+])\s+")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# --------------------------------------------------------------------------
|
|
104
|
+
# Text cleaning
|
|
105
|
+
# --------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def strip_inline(text: str) -> str:
|
|
109
|
+
"""Remove markdown atoms that would distort word counts and word scans.
|
|
110
|
+
|
|
111
|
+
Order matters: strip images and links before bare URLs, or the closing
|
|
112
|
+
paren of a link gets eaten by the URL pass.
|
|
113
|
+
"""
|
|
114
|
+
text = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", text)
|
|
115
|
+
text = re.sub(r"\[([^\]]+)\]\([^)]*\)", r"\1", text)
|
|
116
|
+
text = re.sub(r"`[^`\n]+`", "CODESPAN", text)
|
|
117
|
+
text = re.sub(r"https?://[^\s)]+", "URL", text)
|
|
118
|
+
return text
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def strip_code(text: str) -> str:
|
|
122
|
+
"""Strip fenced code blocks, headings, and tables before STE scans."""
|
|
123
|
+
text = re.sub(r"```.*?```", " ", text, flags=re.S)
|
|
124
|
+
text = re.sub(r"~~~.*?~~~", " ", text, flags=re.S)
|
|
125
|
+
text = strip_inline(text)
|
|
126
|
+
text = re.sub(r"^#+\s.*$", " ", text, flags=re.M) # headings exempt (8.6)
|
|
127
|
+
text = re.sub(r"^[|].*$", " ", text, flags=re.M) # table rows exempt
|
|
128
|
+
text = re.sub(r"^\s*([-*]|\d+\.)\s+", "", text, flags=re.M) # list markers
|
|
129
|
+
return text
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def sentences(text: str) -> List[str]:
|
|
133
|
+
"""Split into sentences on terminal punctuation followed by a capital."""
|
|
134
|
+
parts = re.split(r"(?<=[.!?:])\s+(?=[A-Z`\"'])", text)
|
|
135
|
+
return [p.strip() for p in parts if len(p.strip().split()) >= 2]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# --------------------------------------------------------------------------
|
|
139
|
+
# STE checks (file-level counts)
|
|
140
|
+
# --------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def ste_counts(body: str, text_type: str) -> dict:
|
|
144
|
+
"""Return per-violation counts for the STE checks on a cleaned body."""
|
|
145
|
+
limit = LIMITS[text_type]
|
|
146
|
+
sents = sentences(body)
|
|
147
|
+
lengths = [len(s.split()) for s in sents]
|
|
148
|
+
counts: dict = {}
|
|
149
|
+
counts["sentence_over_limit"] = sum(1 for n in lengths if n > limit)
|
|
150
|
+
counts["contraction"] = len(CONTRACTION.findall(body))
|
|
151
|
+
counts["banned_modal"] = len(BANNED_MODALS.findall(body))
|
|
152
|
+
counts["perfect_tense"] = len(PERFECT.findall(body))
|
|
153
|
+
counts["progressive_passive"] = len(PROGRESSIVE_PASSIVE.findall(body))
|
|
154
|
+
counts["ing_clause"] = len(ING_CLAUSE.findall(body))
|
|
155
|
+
counts["semicolon"] = body.count(";")
|
|
156
|
+
counts["latin_abbrev"] = len(LATIN.findall(body))
|
|
157
|
+
counts["slop_word"] = len(SLOP.findall(body))
|
|
158
|
+
counts["trailing_condition"] = sum(
|
|
159
|
+
1
|
|
160
|
+
for s in sents
|
|
161
|
+
if TRAILING_COND.search(s) and not re.match(r"^(if|when)\b", s, re.I)
|
|
162
|
+
)
|
|
163
|
+
counts["positional_reference"] = len(POSITIONAL.findall(body))
|
|
164
|
+
|
|
165
|
+
rotation = 0
|
|
166
|
+
for syn_set in SYNONYM_SETS:
|
|
167
|
+
used = [
|
|
168
|
+
w
|
|
169
|
+
for w in syn_set
|
|
170
|
+
if len(re.findall(rf"\b{re.escape(w)}\b", body, re.I)) >= 2
|
|
171
|
+
]
|
|
172
|
+
if len(used) >= 2:
|
|
173
|
+
rotation += len(used) - 1
|
|
174
|
+
counts["synonym_rotation"] = rotation
|
|
175
|
+
|
|
176
|
+
words = max(1, len(body.split()))
|
|
177
|
+
total = sum(counts.values())
|
|
178
|
+
return {
|
|
179
|
+
"type": text_type,
|
|
180
|
+
"words": words,
|
|
181
|
+
"sentences": len(sents),
|
|
182
|
+
"mean_sentence_words": round(sum(lengths) / max(1, len(lengths)), 1),
|
|
183
|
+
"longest_sentence_words": max(lengths, default=0),
|
|
184
|
+
"violations": counts,
|
|
185
|
+
"violations_total": total,
|
|
186
|
+
"violations_per_100w": round(100.0 * total / words, 2),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# --------------------------------------------------------------------------
|
|
191
|
+
# Structural checks (line-level, file-aware) — from check_docs.py
|
|
192
|
+
# --------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
# Severity: "error" fails the gate; "warning" does not.
|
|
195
|
+
POSITIONAL_FINDING = ("warning", "positional reference breaks under retrieval; link to the section")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def lint_structural(path: Path, lines: List[str]) -> List[Tuple[int, str, str]]:
|
|
199
|
+
"""Heading-hierarchy + link-text checks. Returns (line_no, level, msg)."""
|
|
200
|
+
findings: List[Tuple[int, str, str]] = []
|
|
201
|
+
in_code = False
|
|
202
|
+
levels_seen: List[int] = []
|
|
203
|
+
headings_text: List[str] = []
|
|
204
|
+
h1_count = 0
|
|
205
|
+
for i, raw in enumerate(lines, 1):
|
|
206
|
+
if CODE_FENCE.match(raw.strip()):
|
|
207
|
+
in_code = not in_code
|
|
208
|
+
continue
|
|
209
|
+
if in_code:
|
|
210
|
+
continue
|
|
211
|
+
m = HEADING.match(raw)
|
|
212
|
+
if m:
|
|
213
|
+
level, text = len(m.group(1)), m.group(2).strip()
|
|
214
|
+
if level == 1:
|
|
215
|
+
h1_count += 1
|
|
216
|
+
if h1_count > 1:
|
|
217
|
+
findings.append((i, "error", "multiple h1 headings on one page"))
|
|
218
|
+
if levels_seen and level > levels_seen[-1] + 1:
|
|
219
|
+
findings.append(
|
|
220
|
+
(i, "error", f"skipped heading level (h{levels_seen[-1]} -> h{level})")
|
|
221
|
+
)
|
|
222
|
+
levels_seen.append(level)
|
|
223
|
+
low = text.lower()
|
|
224
|
+
if low in headings_text:
|
|
225
|
+
findings.append(
|
|
226
|
+
(i, "warning", f"duplicate heading '{text}' (anchors collide)")
|
|
227
|
+
)
|
|
228
|
+
headings_text.append(low)
|
|
229
|
+
continue
|
|
230
|
+
for link_m in MD_LINK.finditer(raw):
|
|
231
|
+
label = link_m.group(1).strip().lower()
|
|
232
|
+
target = link_m.group(2)
|
|
233
|
+
if label in ("here", "this", "link", "this page"):
|
|
234
|
+
findings.append((i, "error", f"non-descriptive link text '{label}'"))
|
|
235
|
+
if not target.startswith(("http://", "https://", "mailto:", "/")):
|
|
236
|
+
if not (path.parent / target).exists():
|
|
237
|
+
findings.append((i, "error", f"broken relative link '{target}'"))
|
|
238
|
+
return findings
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# --------------------------------------------------------------------------
|
|
242
|
+
# Linter
|
|
243
|
+
# --------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class Linter:
|
|
247
|
+
def __init__(self, text_type: str, use_vale: bool, style: bool):
|
|
248
|
+
self.text_type = text_type
|
|
249
|
+
self.vale = use_vale and shutil.which("vale") is not None
|
|
250
|
+
# Built-in style checks run only when Vale does not take the style layer.
|
|
251
|
+
self.style = style and not self.vale
|
|
252
|
+
self.findings: List[Tuple[Path, int, str, str]] = []
|
|
253
|
+
|
|
254
|
+
def add(self, path: Path, line: int, level: str, msg: str) -> None:
|
|
255
|
+
self.findings.append((path, line, level, msg))
|
|
256
|
+
|
|
257
|
+
def lint_file(self, path: Path) -> None:
|
|
258
|
+
raw_text = path.read_text(encoding="utf-8", errors="replace")
|
|
259
|
+
lines = raw_text.splitlines()
|
|
260
|
+
|
|
261
|
+
# --- STE checks on the cleaned body ---
|
|
262
|
+
body = strip_code(raw_text)
|
|
263
|
+
rep = ste_counts(body, self.text_type)
|
|
264
|
+
for key, n in rep["violations"].items():
|
|
265
|
+
if n <= 0:
|
|
266
|
+
continue
|
|
267
|
+
level = "error" if key in ("banned_modal", "contraction") else "warning"
|
|
268
|
+
self.add(path, 0, level, f"{key}: {n}")
|
|
269
|
+
|
|
270
|
+
# hard error on runaway sentences regardless of mode
|
|
271
|
+
if rep["longest_sentence_words"] > LONG_SENTENCE_ERROR:
|
|
272
|
+
self.add(
|
|
273
|
+
path,
|
|
274
|
+
0,
|
|
275
|
+
"error",
|
|
276
|
+
f"sentence has {rep['longest_sentence_words']} words "
|
|
277
|
+
f"(hard limit {LONG_SENTENCE_ERROR})",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# --- Structural checks (heading hierarchy, link text) ---
|
|
281
|
+
for line_no, level, msg in lint_structural(path, lines):
|
|
282
|
+
self.add(path, line_no, level, msg)
|
|
283
|
+
|
|
284
|
+
# --- Built-in style layer (only when Vale is absent) ---
|
|
285
|
+
if self.style:
|
|
286
|
+
for sent in sentences(strip_inline(raw_text)):
|
|
287
|
+
if POSITIONAL.search(sent):
|
|
288
|
+
self.add(path, 0, POSITIONAL_FINDING[0], POSITIONAL_FINDING[1])
|
|
289
|
+
|
|
290
|
+
def run_vale(self, target: Path) -> None:
|
|
291
|
+
ini = None
|
|
292
|
+
for parent in [target, *list(target.parents)]:
|
|
293
|
+
if (parent / ".vale.ini").exists():
|
|
294
|
+
ini = parent / ".vale.ini"
|
|
295
|
+
break
|
|
296
|
+
if ini is None:
|
|
297
|
+
# No config: fall back to built-in style checks.
|
|
298
|
+
self.style = True
|
|
299
|
+
self.vale = False
|
|
300
|
+
return
|
|
301
|
+
r = subprocess.run(
|
|
302
|
+
["vale", "--output=line", str(target)],
|
|
303
|
+
capture_output=True,
|
|
304
|
+
text=True,
|
|
305
|
+
cwd=str(ini.parent),
|
|
306
|
+
)
|
|
307
|
+
for line in r.stdout.splitlines():
|
|
308
|
+
print(line)
|
|
309
|
+
if r.returncode != 0:
|
|
310
|
+
self.findings.append((target, 0, "error", "vale reported errors (above)"))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# --------------------------------------------------------------------------
|
|
314
|
+
# Self-test (used by the skill's `→ verify:` gate)
|
|
315
|
+
# --------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
SLOP_FIXTURE = """Leveraging our robust retry mechanism, failed uploads are automatically
|
|
318
|
+
reattempted, ensuring data integrity is maintained throughout the entire process which has
|
|
319
|
+
been designed from the ground up to gracefully handle even the most challenging network
|
|
320
|
+
interruptions. You should verify your credentials; it's also worth checking the settings,
|
|
321
|
+
e.g. the timeout config. Contact support if the problem persists."""
|
|
322
|
+
|
|
323
|
+
CLEAN_FIXTURE = """The system retries a failed upload automatically. This process keeps the data correct.
|
|
324
|
+
|
|
325
|
+
If failures continue, make sure that your credentials are correct. If the problem continues, contact support."""
|
|
326
|
+
|
|
327
|
+
# Genuine terminology drift: two synonyms (check, verify) each used twice.
|
|
328
|
+
DRIFT_FIXTURE = """To check the build, run the test suite. Then verify the output log.
|
|
329
|
+
Check the exit code. Verify the result against the baseline."""
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def self_test() -> None:
|
|
333
|
+
"""Assert the linter catches what it must and clears what it should."""
|
|
334
|
+
slop = ste_counts(strip_code(SLOP_FIXTURE), "procedural")
|
|
335
|
+
clean = ste_counts(strip_code(CLEAN_FIXTURE), "procedural")
|
|
336
|
+
drift = ste_counts(strip_code(DRIFT_FIXTURE), "procedural")
|
|
337
|
+
assert slop["violations"]["sentence_over_limit"] >= 1, slop
|
|
338
|
+
assert slop["violations"]["banned_modal"] >= 1, slop
|
|
339
|
+
assert slop["violations"]["contraction"] >= 1, slop
|
|
340
|
+
assert slop["violations"]["perfect_tense"] >= 1, slop
|
|
341
|
+
assert slop["violations"]["ing_clause"] >= 1, slop
|
|
342
|
+
assert slop["violations"]["semicolon"] >= 1, slop
|
|
343
|
+
assert slop["violations"]["latin_abbrev"] >= 1, slop
|
|
344
|
+
assert slop["violations"]["slop_word"] >= 2, slop
|
|
345
|
+
assert slop["violations"]["trailing_condition"] >= 1, slop
|
|
346
|
+
# The slop fixture mentions "verify" and "checking" only once each, so the
|
|
347
|
+
# strict drift detector (2+ synonyms each used 2+ times) must stay silent.
|
|
348
|
+
assert slop["violations"]["synonym_rotation"] == 0, slop
|
|
349
|
+
assert drift["violations"]["synonym_rotation"] >= 1, drift
|
|
350
|
+
assert clean["violations_total"] == 0, clean
|
|
351
|
+
|
|
352
|
+
# Structural: duplicate headings and a skipped level must flag.
|
|
353
|
+
struct = lint_structural(
|
|
354
|
+
Path("x"),
|
|
355
|
+
["# Title", "## Sub", "#### Skipped", "#### Skipped", ""],
|
|
356
|
+
)
|
|
357
|
+
assert any("skipped heading" in m for _, _, m in struct), struct
|
|
358
|
+
assert any("duplicate heading" in m for _, _, m in struct), struct
|
|
359
|
+
|
|
360
|
+
# Non-descriptive link text must flag as an error.
|
|
361
|
+
link_findings = lint_structural(Path("x"), ["See [here](./x.md) for more."])
|
|
362
|
+
assert any(lvl == "error" for _, lvl, _ in link_findings), link_findings
|
|
363
|
+
|
|
364
|
+
print(
|
|
365
|
+
"self-test OK: "
|
|
366
|
+
f"{slop['violations_total']} violations in slop fixture, "
|
|
367
|
+
"0 in clean, structural checks pass"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# --------------------------------------------------------------------------
|
|
372
|
+
# CLI
|
|
373
|
+
# --------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def gather_files(target: Path) -> List[Path]:
|
|
377
|
+
if target.is_file():
|
|
378
|
+
return [target]
|
|
379
|
+
return sorted(target.rglob("*.md"))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def main(argv: List[str] | None = None) -> int:
|
|
383
|
+
ap = argparse.ArgumentParser(description="STE machine gate.")
|
|
384
|
+
ap.add_argument("target", nargs="?", help="file or dir to lint, or '-' for stdin")
|
|
385
|
+
ap.add_argument("--type", choices=sorted(LIMITS), default=DEFAULT_TYPE)
|
|
386
|
+
ap.add_argument("--json", action="store_true", help="emit JSON report only")
|
|
387
|
+
ap.add_argument("--no-vale", action="store_true", help="force built-in style checks")
|
|
388
|
+
ap.add_argument("--self-test", action="store_true", help="run built-in assertions and exit")
|
|
389
|
+
args = ap.parse_args(argv)
|
|
390
|
+
|
|
391
|
+
if args.self_test:
|
|
392
|
+
self_test()
|
|
393
|
+
return 0
|
|
394
|
+
|
|
395
|
+
if args.target is None or args.target == "":
|
|
396
|
+
ap.error("the following arguments are required: target")
|
|
397
|
+
|
|
398
|
+
# stdin path
|
|
399
|
+
if args.target == "-":
|
|
400
|
+
text = sys.stdin.read()
|
|
401
|
+
rep = ste_counts(strip_code(text), args.type)
|
|
402
|
+
print(json.dumps(rep, indent=2))
|
|
403
|
+
return 0 if rep["violations_total"] == 0 else 1
|
|
404
|
+
|
|
405
|
+
target = Path(args.target).resolve()
|
|
406
|
+
if not target.exists():
|
|
407
|
+
print(f"target not found: {target}", file=sys.stderr)
|
|
408
|
+
return 2
|
|
409
|
+
|
|
410
|
+
files = gather_files(target)
|
|
411
|
+
if not files:
|
|
412
|
+
print(f"no markdown files under {target}")
|
|
413
|
+
return 0
|
|
414
|
+
|
|
415
|
+
linter = Linter(args.type, use_vale=not args.no_vale, style=True)
|
|
416
|
+
if linter.vale and target.is_dir():
|
|
417
|
+
linter.run_vale(target)
|
|
418
|
+
for f in files:
|
|
419
|
+
linter.lint_file(f)
|
|
420
|
+
|
|
421
|
+
if args.json:
|
|
422
|
+
rep = {
|
|
423
|
+
"files": len(files),
|
|
424
|
+
"findings": [
|
|
425
|
+
{"path": str(p), "line": n, "level": lvl, "msg": msg}
|
|
426
|
+
for (p, n, lvl, msg) in linter.findings
|
|
427
|
+
],
|
|
428
|
+
}
|
|
429
|
+
print(json.dumps(rep, indent=2))
|
|
430
|
+
else:
|
|
431
|
+
for path, line, level, msg in linter.findings:
|
|
432
|
+
loc = f"{path}:{line}" if line else f"{path}"
|
|
433
|
+
print(f"{loc}: [{level}] {msg}")
|
|
434
|
+
|
|
435
|
+
errors = sum(1 for *_, lvl, _ in linter.findings if lvl == "error")
|
|
436
|
+
warnings = sum(1 for *_, lvl, _ in linter.findings if lvl == "warning")
|
|
437
|
+
style_layer = "vale" if linter.vale else "built-in"
|
|
438
|
+
print(
|
|
439
|
+
f"\n{errors} error(s), {warnings} warning(s) in {len(files)} file(s) "
|
|
440
|
+
f"[style layer: {style_layer}]"
|
|
441
|
+
)
|
|
442
|
+
return 1 if errors else 0
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
if __name__ == "__main__":
|
|
446
|
+
sys.exit(main())
|
package/skills-lock.json
CHANGED
|
@@ -321,6 +321,11 @@
|
|
|
321
321
|
"sha256": "33bc4ecbd63cc9ca",
|
|
322
322
|
"path": "skills/setup-environmentSKILL.md"
|
|
323
323
|
},
|
|
324
|
+
"simple-english": {
|
|
325
|
+
"description": "Write or rewrite technical text with the rules of ASD-STE100 Simplified Technical English so the result is clear, unambiguous, and free of AI slop. Use for documentation, READMEs, runbooks, procedures, error messages, release notes, incident reports, and API guides. Also use when the user says \"STE\", \"Simplified Technical English\", \"ASD-STE100\", \"de-slop\", \"make this readable\", \"write for non-native readers\", or asks for docs that translate well. Enforces the 53 rules of the standard (Issue 9) with a deterministic lint gate.",
|
|
326
|
+
"sha256": "a0e904d86d8a7bc3",
|
|
327
|
+
"path": "skills/simple-englishSKILL.md"
|
|
328
|
+
},
|
|
324
329
|
"simulate-agents": {
|
|
325
330
|
"description": "Run Mock User and Auditor agents against a feature in fresh contexts before human review. Use after verify-work, before request-review, when user wants pre-review simulation.",
|
|
326
331
|
"sha256": "499159f692b09b27",
|