Jev maintenance tools: shared client, spec coverage audit, voice check, nightly triage
This commit is contained in:
1 parent
0ff888e41b
commit
3ed0503bdf
9 files changed
+722
-48
No files matched your search
@@ -0,0 +1,152 @@
|
||||
"""Check public texts against the brand voice in branding/README.md: plain, exact, every claim next to its limitation.
|
||||
|
||||
Code checks the mechanical rules (em and en dashes in prose, banned marketing words). For every paragraph of at least
|
||||
15 words, Jev answers three questions in one request: does the paragraph claim something about SimurgArray without
|
||||
any limitation or condition (Noul), does it use promotional exaggeration (Noul), and how plain is the tone (Score,
|
||||
four concrete levels). The tool marks paragraphs to look at; it never rewrites them.
|
||||
|
||||
python scripts/jev/voice_check.py README.md blog/*.md my_post.txt # writes voice_report.md
|
||||
python scripts/jev/voice_check.py --rules-only blog/*.md # mechanical rules only, no API calls
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from client import ROOT, noul, score, system_one
|
||||
|
||||
BANNED = ["revolutionary", "cutting edge", "cutting-edge", "ai powered", "ai-powered", "next generation",
|
||||
"next-generation", "seamless", "seamlessly", "unlock", "game changer", "game-changing", "best in class",
|
||||
"world class", "blazing"] # fmt: skip
|
||||
LIMIT_T, HYPE_T, TONE_T = 0.7, 0.6, 1.5
|
||||
TONE_LEVELS = [
|
||||
"Plain and exact: short sentences, concrete facts or numbers, no salesmanship",
|
||||
"Mostly plain, with an occasional vague or enthusiastic phrase",
|
||||
"Noticeably promotional or vague: several adjectives or benefits stated without specifics",
|
||||
"Marketing copy: superlatives, buzzwords and promises dominate",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Para:
|
||||
path: str
|
||||
line: int
|
||||
heading: str
|
||||
text: str
|
||||
issues: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def paragraphs(path: Path, text: str) -> list[Para]:
|
||||
"""Prose paragraphs and bullets; code blocks, tables, headings and front matter lines are skipped."""
|
||||
out: list[Para] = []
|
||||
heading, buf, start, in_code = "", [], 0, False
|
||||
|
||||
def flush() -> None:
|
||||
if buf:
|
||||
out.append(Para(str(path), start, heading, " ".join(buf)))
|
||||
buf.clear()
|
||||
|
||||
for i, raw in enumerate([*text.splitlines(), ""], 1):
|
||||
ln = raw.rstrip()
|
||||
if ln.lstrip().startswith("```"):
|
||||
flush()
|
||||
in_code = not in_code
|
||||
continue
|
||||
if in_code:
|
||||
continue
|
||||
if not ln.strip() or ln.lstrip().startswith("|") or ln.startswith("#"):
|
||||
flush()
|
||||
if ln.startswith("#"):
|
||||
heading = ln.lstrip("#").strip()
|
||||
continue
|
||||
m = re.match(r"\s*([*>-]|\d+\.)\s+", ln)
|
||||
if m:
|
||||
flush()
|
||||
ln = ln[m.end() :]
|
||||
if ln.lstrip().startswith("<"): # raw HTML blocks (banners, badges) are layout, not prose
|
||||
continue
|
||||
if not buf:
|
||||
start = i
|
||||
buf.append(ln.strip())
|
||||
return out
|
||||
|
||||
|
||||
def mechanical(p: Para) -> list[str]:
|
||||
prose = re.sub(r"`[^`]*`|https?://\S+|\([^)]*\.(md|png|jpg|svg)\)", "", p.text)
|
||||
prose = re.sub(r"\"[^\"]*\"|\u201c[^\u201d]*\u201d", "", prose) # quoted words are examples, not usage
|
||||
if re.match(r"\s*(Never|No|Do not)\b", prose):
|
||||
prose = ""
|
||||
issues = []
|
||||
if re.search("[—–]", prose):
|
||||
issues.append("em or en dash in prose")
|
||||
low = prose.lower()
|
||||
found = [w for w in BANNED if re.search(rf"\b{re.escape(w)}\b", low)]
|
||||
if found:
|
||||
issues.append("banned words: " + ", ".join(found))
|
||||
return issues
|
||||
|
||||
|
||||
QUESTIONS = {
|
||||
"no_limit": noul(
|
||||
"Does `paragraph` claim that SimurgArray achieves, guarantees or provides something (accuracy, speed, "
|
||||
"compatibility, safety, a benefit) without stating any limitation, condition, number or caveat in the same "
|
||||
"paragraph?",
|
||||
true="A capability or benefit is asserted with nothing that bounds it",
|
||||
false="There is no such claim, or the claim comes with a number, condition or limitation",
|
||||
),
|
||||
"hype": noul(
|
||||
"Does `paragraph` use promotional exaggeration: superlatives, buzzwords or unqualified promises instead of "
|
||||
"plain description?",
|
||||
true="Contains marketing language or overstatement",
|
||||
false="Plain description, even if enthusiastic in a modest way",
|
||||
),
|
||||
"tone": score("How plain and exact is the tone of `paragraph`?", TONE_LEVELS),
|
||||
}
|
||||
|
||||
|
||||
def judge(p: Para, doc: str) -> dict[str, float]:
|
||||
ans = system_one({"document": doc, "section": p.heading, "paragraph": p.text}, QUESTIONS)
|
||||
return {"no_limit": float(ans["no_limit"]["noul"]), "hype": float(ans["hype"]["noul"]),
|
||||
"tone": float(ans["tone"]["score"])} # fmt: skip
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
||||
ap.add_argument("files", nargs="+", type=Path)
|
||||
ap.add_argument("--out", type=Path, default=ROOT / "voice_report.md")
|
||||
ap.add_argument("--rules-only", action="store_true", help="mechanical rules only, no API calls")
|
||||
a = ap.parse_args(argv)
|
||||
flagged: list[tuple[Para, dict[str, float]]] = []
|
||||
n = 0
|
||||
for f in a.files:
|
||||
doc = f.name
|
||||
for p in paragraphs(f, f.read_text(encoding="utf-8")):
|
||||
p.issues = mechanical(p)
|
||||
vals: dict[str, float] = {}
|
||||
if not a.rules_only and len(p.text.split()) >= 15:
|
||||
n += 1
|
||||
vals = judge(p, doc)
|
||||
if vals["no_limit"] >= LIMIT_T:
|
||||
p.issues.append(f"claim without a limitation (p {vals['no_limit']:.2f})")
|
||||
if vals["hype"] >= HYPE_T:
|
||||
p.issues.append(f"promotional language (p {vals['hype']:.2f})")
|
||||
if vals["tone"] >= TONE_T:
|
||||
p.issues.append(f"tone {vals['tone']:.1f} of 3")
|
||||
if p.issues:
|
||||
flagged.append((p, vals))
|
||||
lines = ["# Voice check", "", f"{n} paragraphs judged, {len(flagged)} flagged.", ""]
|
||||
for p, _ in flagged:
|
||||
lines.append(f"* `{p.path}:{p.line}` ({p.heading}) **{'; '.join(p.issues)}**")
|
||||
lines.append(f" > {p.text[:400]}")
|
||||
a.out.write_text("\n".join(lines) + "\n")
|
||||
print(f"{n} paragraphs judged, {len(flagged)} flagged → {a.out}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user