update skills
This commit is contained in:
parent
0f4f823f2a
commit
d4436483a9
|
|
@ -28,9 +28,9 @@ Produces English text artifacts under Oleg's name: cover letters, application es
|
|||
Every generated text **must** pass through the voice linter before it reaches Oleg.
|
||||
|
||||
```bash
|
||||
python3 .claude/skills/write-as-oleg/lint-voice.py "full text here"
|
||||
node .claude/skills/write-as-oleg/lint-voice.mjs "full text here"
|
||||
# or via stdin:
|
||||
echo "full text" | python3 .claude/skills/write-as-oleg/lint-voice.py
|
||||
echo "full text" | node .claude/skills/write-as-oleg/lint-voice.mjs
|
||||
```
|
||||
|
||||
**Loop protocol:**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,265 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* lint-voice.mjs — deterministic voice linter for Oleg's texts.
|
||||
*
|
||||
* Checks for AI/corporate artifacts, wrong typography, and banned patterns
|
||||
* that make text sound like a model rather than Oleg.
|
||||
*
|
||||
* Usage:
|
||||
* node lint-voice.mjs "text to check"
|
||||
* echo "text" | node lint-voice.mjs
|
||||
* node lint-voice.mjs < input.txt
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 — clean (no ERRORs; WARNs alone do not fail)
|
||||
* 1 — ERRORs found (agent must fix and re-run)
|
||||
* 2 — usage error
|
||||
*/
|
||||
|
||||
// [level, id, pattern, message]
|
||||
// level: "ERROR" = must fix before delivery; "WARN" = check in context, may be OK
|
||||
const CHECKS = [
|
||||
// ── Typography ─────────────────────────────────────────────────────────────
|
||||
["ERROR", "EM_DASH",
|
||||
/—/g,
|
||||
"Em-dash (—). Replace with colon, comma, or rephrase as separate sentence."],
|
||||
|
||||
["ERROR", "SMART_QUOTES",
|
||||
/[""„«»]/g,
|
||||
'Smart/typographic double quote. Use straight " or rephrase.'],
|
||||
|
||||
["WARN", "SMART_APOSTROPHE",
|
||||
/[''`]/g,
|
||||
"Smart/curly apostrophe or backtick. Use straight apostrophe '."],
|
||||
|
||||
// ── AI / corporate words ───────────────────────────────────────────────────
|
||||
["ERROR", "AI_LEVERAGE",
|
||||
/\bleverag(?:e|es|ed|ing)\b/gi,
|
||||
'"leverage" — AI/corporate word. Replace with plain verb (use, apply, build on).'],
|
||||
|
||||
["ERROR", "AI_ROBUST",
|
||||
/\brobust\b/gi,
|
||||
'"robust" — AI/corporate word. Replace with specific description.'],
|
||||
|
||||
["ERROR", "AI_SEAMLESS",
|
||||
/\bseamless(?:ly)?\b/gi,
|
||||
'"seamless" — AI/corporate word. Cut or replace with plain description.'],
|
||||
|
||||
["ERROR", "AI_PASSIONATE",
|
||||
/\bpassionat(?:e|ely)\b|\bmy passion\b/gi,
|
||||
'"passionate/passion" — AI/corporate word. Replace with direct statement.'],
|
||||
|
||||
["ERROR", "AI_THRILLED",
|
||||
/\bthrilled\b/gi,
|
||||
'"thrilled" — AI word. Replace with direct statement.'],
|
||||
|
||||
["ERROR", "AI_DELVE",
|
||||
/\bdelv(?:e|ing|ed)\b/gi,
|
||||
'"delve" — AI word. Replace with explore / look into / dig.'],
|
||||
|
||||
["ERROR", "AI_TAPESTRY",
|
||||
/\btapestry\b/gi,
|
||||
'"tapestry" — AI word. Remove or replace.'],
|
||||
|
||||
["ERROR", "AI_EMPOWER",
|
||||
/\bempower(?:s|ed|ing)?\b/gi,
|
||||
'"empower" — AI/corporate word. Replace with plain verb.'],
|
||||
|
||||
["ERROR", "AI_STREAMLINE",
|
||||
/\bstreamline[ds]?\b|\bstreamlining\b/gi,
|
||||
'"streamline" — AI/corporate word. Replace with plain verb.'],
|
||||
|
||||
["ERROR", "AI_ACTIONABLE",
|
||||
/\bactionable\b/gi,
|
||||
'"actionable" — AI/corporate word. Replace with plain description.'],
|
||||
|
||||
["ERROR", "AI_UNLOCK",
|
||||
/\bunlock(?:s|ed|ing)?\b/gi,
|
||||
'"unlock" — AI/corporate word. Replace with plain verb.'],
|
||||
|
||||
["ERROR", "AI_CUTTING_EDGE",
|
||||
/cutting[- ]edge/gi,
|
||||
'"cutting-edge" — AI/corporate phrase. Replace with specific description.'],
|
||||
|
||||
["WARN", "AI_INNOVATIVE",
|
||||
/\binnovativ(?:e|ely)\b|\binnovation\b/gi,
|
||||
'"innovative/innovation" — often AI/corporate filler. Replace with specific if used generically.'],
|
||||
|
||||
// ── Sentence openers (capitalised opener form) ────────────────────────────
|
||||
// Matches word at line start OR after terminal punctuation + whitespace
|
||||
["ERROR", "OPENER_FURTHERMORE",
|
||||
/(?:^|(?<=[.!?])[ \t]+)Furthermore[,\s]/gm,
|
||||
'"Furthermore" opener. Cut — connection should come from content, not connective.'],
|
||||
|
||||
["ERROR", "OPENER_MOREOVER",
|
||||
/(?:^|(?<=[.!?])[ \t]+)Moreover[,\s]/gm,
|
||||
'"Moreover" opener. Cut — restructure.'],
|
||||
|
||||
["ERROR", "OPENER_ADDITIONALLY",
|
||||
/(?:^|(?<=[.!?])[ \t]+)Additionally[,\s]/gm,
|
||||
'"Additionally" opener. Cut — restructure without connective.'],
|
||||
|
||||
["ERROR", "OPENER_ULTIMATELY",
|
||||
/(?:^|(?<=[.!?])[ \t]+)Ultimately[,\s]/gm,
|
||||
'"Ultimately" opener. Cut — restructure.'],
|
||||
|
||||
["ERROR", "OPENER_HOWEVER",
|
||||
/(?:^|(?<=[.!?])[ \t]+)However[,\s]/gm,
|
||||
'"However" opener. Cut — use contrast from content, not a gluing connective.'],
|
||||
|
||||
["ERROR", "OPENER_IMPORTANTLY",
|
||||
/(?:^|(?<=[.!?])[ \t]+)Importantly[,\s]/gm,
|
||||
'"Importantly" opener — filler frame. Cut.'],
|
||||
|
||||
["ERROR", "OPENER_THAT_SAID",
|
||||
/(?:^|(?<=[.!?])[ \t]+)That said[,\s]/gm,
|
||||
'"That said" opener. Cut — restructure.'],
|
||||
|
||||
["ERROR", "OPENER_IN_CONCLUSION",
|
||||
/in conclusion[,\s]/gi,
|
||||
'"In conclusion" — empty closing summary. Cut entirely.'],
|
||||
|
||||
["ERROR", "OPENER_OVERALL",
|
||||
/(?:^|(?<=[.!?])[ \t]+)Overall[,\s]/gm,
|
||||
'"Overall" opener — empty summary signal. Cut.'],
|
||||
|
||||
["WARN", "OPENER_SO_COMMA",
|
||||
/(?:^|(?<=[.!?])[ \t]+)So[,\s]/gm,
|
||||
'"So," opener. Oleg cuts these — connection should come from content.'],
|
||||
|
||||
// ── Filler frames ──────────────────────────────────────────────────────────
|
||||
["ERROR", "FILLER_WORTH_NOTING",
|
||||
/it'?s worth noting/gi,
|
||||
'"It\'s worth noting" — filler frame. Cut, say the thing directly.'],
|
||||
|
||||
["ERROR", "FILLER_IMPORTANT_REMEMBER",
|
||||
/it'?s important to remember/gi,
|
||||
'"It\'s important to remember" — filler frame. Cut.'],
|
||||
|
||||
["ERROR", "FILLER_I_AM_WRITING",
|
||||
/\bI am writing to\b/gi,
|
||||
'"I am writing to" — generic opener. Replace with direct lead.'],
|
||||
|
||||
["ERROR", "FILLER_IN_TODAYS",
|
||||
/in today'?s\b/gi,
|
||||
'"In today\'s..." — AI/generic opener. Cut.'],
|
||||
|
||||
// ── AI structural patterns ─────────────────────────────────────────────────
|
||||
["ERROR", "PATTERN_ITS_NOT_JUST",
|
||||
/it'?s not just/gi,
|
||||
'"it\'s not just X, it\'s Y" construction. Rephrase as direct statement.'],
|
||||
|
||||
["WARN", "PATTERN_NOT_X_BUT",
|
||||
/\bnot \w+(?:\s+\w+){0,3},?\s+but\b/gi,
|
||||
'"not X, but Y" — often AI phrasing. Rephrase if rhetorical.'],
|
||||
|
||||
["ERROR", "PATTERN_HONEST_FRAMING",
|
||||
/honest framing|one thing to flag|I want to be honest about/gi,
|
||||
'"honest framing / one thing to flag" template. Not Oleg\'s pattern — remove.'],
|
||||
|
||||
// ── AI closings ────────────────────────────────────────────────────────────
|
||||
["ERROR", "CLOSING_LOOKING_FORWARD",
|
||||
/looking forward to/gi,
|
||||
'"Looking forward to" closing. Replace e.g. "Open to chat if it\'s a fit."'],
|
||||
|
||||
["ERROR", "CLOSING_HAPPY_TO",
|
||||
/\bhappy to\b/gi,
|
||||
'"happy to" — AI phrasing. Replace with plain statement.'],
|
||||
|
||||
// ── Warn-only borderline cases ─────────────────────────────────────────────
|
||||
["WARN", "WARN_NAVIGATE",
|
||||
/\bnavigat(?:e|ing|ed)\b/gi,
|
||||
'"navigate" — check if metaphorical AI-speak ("navigate challenges"). OK if literal.'],
|
||||
|
||||
["WARN", "WARN_LANDSCAPE",
|
||||
/\blandscape\b/gi,
|
||||
'"landscape" — check if metaphorical AI-speak. OK if literal/technical.'],
|
||||
];
|
||||
|
||||
function lineOf(text, pos) {
|
||||
return text.slice(0, pos).split("\n").length;
|
||||
}
|
||||
|
||||
function getContext(text, start, end, window = 60) {
|
||||
const from = Math.max(0, start - window);
|
||||
const to = Math.min(text.length, end + window);
|
||||
let snippet = text.slice(from, to).replace(/\s+/g, " ").trim();
|
||||
if (from > 0) snippet = "…" + snippet;
|
||||
if (to < text.length) snippet = snippet + "…";
|
||||
return snippet;
|
||||
}
|
||||
|
||||
function runChecks(text) {
|
||||
const findings = [];
|
||||
for (const [level, id, pattern, message] of CHECKS) {
|
||||
// Reset lastIndex before each use (patterns are defined with /g)
|
||||
pattern.lastIndex = 0;
|
||||
let m;
|
||||
while ((m = pattern.exec(text)) !== null) {
|
||||
findings.push({
|
||||
level,
|
||||
id,
|
||||
line: lineOf(text, m.index),
|
||||
context: getContext(text, m.index, m.index + m[0].length),
|
||||
message,
|
||||
});
|
||||
// Prevent infinite loops on zero-width matches
|
||||
if (m[0].length === 0) pattern.lastIndex++;
|
||||
}
|
||||
}
|
||||
findings.sort((a, b) => a.line - b.line || a.id.localeCompare(b.id));
|
||||
return findings;
|
||||
}
|
||||
|
||||
async function readStdin() {
|
||||
const chunks = [];
|
||||
for await (const chunk of process.stdin) chunks.push(chunk);
|
||||
return Buffer.concat(chunks).toString("utf8");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let text;
|
||||
|
||||
if (process.argv.length > 2) {
|
||||
text = process.argv.slice(2).join(" ");
|
||||
} else if (!process.stdin.isTTY) {
|
||||
text = await readStdin();
|
||||
} else {
|
||||
process.stderr.write('Usage: node lint-voice.mjs "text" OR pipe text via stdin\n');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (!text.trim()) {
|
||||
process.stdout.write("✓ CLEAN — empty input.\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const findings = runChecks(text);
|
||||
const errors = findings.filter(f => f.level === "ERROR");
|
||||
const warns = findings.filter(f => f.level === "WARN");
|
||||
|
||||
if (findings.length === 0) {
|
||||
process.stdout.write("✓ CLEAN — no issues found.\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.stdout.write(`Found ${findings.length} issue(s): ${errors.length} ERROR, ${warns.length} WARN\n\n`);
|
||||
|
||||
for (const f of findings) {
|
||||
const action = f.level === "ERROR" ? "Fix" : "Note";
|
||||
process.stdout.write(`[${f.level}] ${f.id} — line ${f.line}\n`);
|
||||
process.stdout.write(` Context: "${f.context}"\n`);
|
||||
process.stdout.write(` ${action}: ${f.message}\n\n`);
|
||||
}
|
||||
|
||||
process.stdout.write("---\n");
|
||||
if (errors.length > 0) {
|
||||
process.stdout.write(`EXIT 1 — ${errors.length} error(s) present. Fix and re-run.\n`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
process.stdout.write("EXIT 0 — no errors (warnings only). Review WARNs if applicable.\n");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
lint-voice.py — deterministic voice linter for Oleg's texts.
|
||||
|
||||
Checks for AI/corporate artifacts, wrong typography, and banned patterns
|
||||
that make text sound like a model rather than Oleg.
|
||||
|
||||
Usage:
|
||||
python lint-voice.py "text to check"
|
||||
echo "text" | python lint-voice.py
|
||||
python lint-voice.py < input.txt
|
||||
|
||||
Exit codes:
|
||||
0 — clean (no ERRORs; WARNs alone do not fail)
|
||||
1 — ERRORs found (agent must fix and re-run)
|
||||
2 — usage error
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
# (level, id, pattern, message)
|
||||
# level: "ERROR" = must fix before delivery; "WARN" = check in context, may be OK
|
||||
CHECKS = [
|
||||
# ── Typography ────────────────────────────────────────────────────────────
|
||||
("ERROR", "EM_DASH",
|
||||
r"—",
|
||||
"Em-dash (—). Replace with colon, comma, or rephrase as separate sentence."),
|
||||
|
||||
("ERROR", "SMART_QUOTES",
|
||||
r'[""„«»]',
|
||||
'Smart/typographic double quote. Use straight " or rephrase.'),
|
||||
|
||||
("WARN", "SMART_APOSTROPHE",
|
||||
r"[''`]",
|
||||
"Smart/curly apostrophe or backtick. Use straight apostrophe '."),
|
||||
|
||||
# ── AI / corporate words ──────────────────────────────────────────────────
|
||||
("ERROR", "AI_LEVERAGE",
|
||||
r"(?i)\bleverag(e|es|ed|ing)\b",
|
||||
'"leverage" — AI/corporate word. Replace with plain verb (use, apply, build on).'),
|
||||
|
||||
("ERROR", "AI_ROBUST",
|
||||
r"(?i)\brobust\b",
|
||||
'"robust" — AI/corporate word. Replace with specific description.'),
|
||||
|
||||
("ERROR", "AI_SEAMLESS",
|
||||
r"(?i)\bseamless(ly)?\b",
|
||||
'"seamless" — AI/corporate word. Cut or replace with plain description.'),
|
||||
|
||||
("ERROR", "AI_PASSIONATE",
|
||||
r"(?i)\bpassionat(e|ely)\b|\bmy passion\b",
|
||||
'"passionate/passion" — AI/corporate word. Replace with direct statement.'),
|
||||
|
||||
("ERROR", "AI_THRILLED",
|
||||
r"(?i)\bthrilled\b",
|
||||
'"thrilled" — AI word. Replace with direct statement.'),
|
||||
|
||||
("ERROR", "AI_DELVE",
|
||||
r"(?i)\bdelv(e|ing|ed)\b",
|
||||
'"delve" — AI word. Replace with explore / look into / dig.'),
|
||||
|
||||
("ERROR", "AI_TAPESTRY",
|
||||
r"(?i)\btapestry\b",
|
||||
'"tapestry" — AI word. Remove or replace.'),
|
||||
|
||||
("ERROR", "AI_EMPOWER",
|
||||
r"(?i)\bempower(s|ed|ing)?\b",
|
||||
'"empower" — AI/corporate word. Replace with plain verb.'),
|
||||
|
||||
("ERROR", "AI_STREAMLINE",
|
||||
r"(?i)\bstreamline[ds]?\b|\bstreamlining\b",
|
||||
'"streamline" — AI/corporate word. Replace with plain verb.'),
|
||||
|
||||
("ERROR", "AI_ACTIONABLE",
|
||||
r"(?i)\bactionable\b",
|
||||
'"actionable" — AI/corporate word. Replace with plain description.'),
|
||||
|
||||
("ERROR", "AI_UNLOCK",
|
||||
r"(?i)\bunlock(s|ed|ing)?\b",
|
||||
'"unlock" — AI/corporate word. Replace with plain verb.'),
|
||||
|
||||
("ERROR", "AI_CUTTING_EDGE",
|
||||
r"(?i)cutting[- ]edge",
|
||||
'"cutting-edge" — AI/corporate phrase. Replace with specific description.'),
|
||||
|
||||
("WARN", "AI_INNOVATIVE",
|
||||
r"(?i)\binnovativ(e|ely)\b|\binnovation\b",
|
||||
'"innovative/innovation" — often AI/corporate filler. Replace with specific if used generically.'),
|
||||
|
||||
# ── Sentence openers (capitalised opener form) ────────────────────────────
|
||||
# Pattern: word at line start OR after terminal punctuation + whitespace
|
||||
("ERROR", "OPENER_FURTHERMORE",
|
||||
r"(?m)(?:^[ \t]*|(?<=[.!?])[ \t]+)Furthermore[,\s]",
|
||||
'"Furthermore" opener. Cut — connection should come from content, not connective.'),
|
||||
|
||||
("ERROR", "OPENER_MOREOVER",
|
||||
r"(?m)(?:^[ \t]*|(?<=[.!?])[ \t]+)Moreover[,\s]",
|
||||
'"Moreover" opener. Cut — restructure.'),
|
||||
|
||||
("ERROR", "OPENER_ADDITIONALLY",
|
||||
r"(?m)(?:^[ \t]*|(?<=[.!?])[ \t]+)Additionally[,\s]",
|
||||
'"Additionally" opener. Cut — restructure without connective.'),
|
||||
|
||||
("ERROR", "OPENER_ULTIMATELY",
|
||||
r"(?m)(?:^[ \t]*|(?<=[.!?])[ \t]+)Ultimately[,\s]",
|
||||
'"Ultimately" opener. Cut — restructure.'),
|
||||
|
||||
("ERROR", "OPENER_HOWEVER",
|
||||
r"(?m)(?:^[ \t]*|(?<=[.!?])[ \t]+)However[,\s]",
|
||||
'"However" opener. Cut — use contrast from content, not a gluing connective.'),
|
||||
|
||||
("ERROR", "OPENER_IMPORTANTLY",
|
||||
r"(?m)(?:^[ \t]*|(?<=[.!?])[ \t]+)Importantly[,\s]",
|
||||
'"Importantly" opener — filler frame. Cut.'),
|
||||
|
||||
("ERROR", "OPENER_THAT_SAID",
|
||||
r"(?m)(?:^[ \t]*|(?<=[.!?])[ \t]+)That said[,\s]",
|
||||
'"That said" opener. Cut — restructure.'),
|
||||
|
||||
("ERROR", "OPENER_IN_CONCLUSION",
|
||||
r"(?i)in conclusion[,\s]",
|
||||
'"In conclusion" — empty closing summary. Cut entirely.'),
|
||||
|
||||
("ERROR", "OPENER_OVERALL",
|
||||
r"(?m)(?:^[ \t]*|(?<=[.!?])[ \t]+)Overall[,\s]",
|
||||
'"Overall" opener — empty summary signal. Cut.'),
|
||||
|
||||
("WARN", "OPENER_SO_COMMA",
|
||||
r"(?m)(?:^[ \t]*|(?<=[.!?])[ \t]+)So[,\s]",
|
||||
'"So," opener. Oleg cuts these — connection should come from content.'),
|
||||
|
||||
# ── Filler frames ─────────────────────────────────────────────────────────
|
||||
("ERROR", "FILLER_WORTH_NOTING",
|
||||
r"(?i)it'?s worth noting",
|
||||
'"It\'s worth noting" — filler frame. Cut, say the thing directly.'),
|
||||
|
||||
("ERROR", "FILLER_IMPORTANT_REMEMBER",
|
||||
r"(?i)it'?s important to remember",
|
||||
'"It\'s important to remember" — filler frame. Cut.'),
|
||||
|
||||
("ERROR", "FILLER_I_AM_WRITING",
|
||||
r"(?i)\bI am writing to\b",
|
||||
'"I am writing to" — generic opener. Replace with direct lead.'),
|
||||
|
||||
("ERROR", "FILLER_IN_TODAYS",
|
||||
r"(?i)in today'?s\b",
|
||||
'"In today\'s..." — AI/generic opener. Cut.'),
|
||||
|
||||
# ── AI structural patterns ────────────────────────────────────────────────
|
||||
("ERROR", "PATTERN_ITS_NOT_JUST",
|
||||
r"(?i)it'?s not just",
|
||||
'"it\'s not just X, it\'s Y" construction. Rephrase as direct statement.'),
|
||||
|
||||
("WARN", "PATTERN_NOT_X_BUT",
|
||||
r"(?i)\bnot [\w]+(?:\s+[\w]+){0,3},?\s+but\b",
|
||||
'"not X, but Y" — often AI phrasing. Rephrase if rhetorical.'),
|
||||
|
||||
("ERROR", "PATTERN_HONEST_FRAMING",
|
||||
r"(?i)honest framing|one thing to flag|I want to be honest about",
|
||||
'"honest framing / one thing to flag" template. Not Oleg\'s pattern — remove.'),
|
||||
|
||||
# ── AI closings ───────────────────────────────────────────────────────────
|
||||
("ERROR", "CLOSING_LOOKING_FORWARD",
|
||||
r"(?i)looking forward to",
|
||||
'"Looking forward to" closing. Replace e.g. "Open to chat if it\'s a fit."'),
|
||||
|
||||
("ERROR", "CLOSING_HAPPY_TO",
|
||||
r"(?i)\bhappy to\b",
|
||||
'"happy to" — AI phrasing. Replace with plain statement.'),
|
||||
|
||||
# ── Warn-only borderline cases ────────────────────────────────────────────
|
||||
("WARN", "WARN_NAVIGATE",
|
||||
r"(?i)\bnavigat(e|ing|ed)\b",
|
||||
'"navigate" — check if metaphorical AI-speak ("navigate challenges"). OK if literal.'),
|
||||
|
||||
("WARN", "WARN_LANDSCAPE",
|
||||
r"(?i)\blandscape\b",
|
||||
'"landscape" — check if metaphorical AI-speak. OK if literal/technical.'),
|
||||
]
|
||||
|
||||
|
||||
def get_context(text: str, match: re.Match, window: int = 60) -> str:
|
||||
start = max(0, match.start() - window)
|
||||
end = min(len(text), match.end() + window)
|
||||
snippet = text[start:end]
|
||||
snippet = " ".join(snippet.split())
|
||||
prefix = "…" if start > 0 else ""
|
||||
suffix = "…" if end < len(text) else ""
|
||||
return f"{prefix}{snippet}{suffix}"
|
||||
|
||||
|
||||
def line_of(text: str, pos: int) -> int:
|
||||
return text[:pos].count("\n") + 1
|
||||
|
||||
|
||||
def run_checks(text: str) -> list[dict]:
|
||||
findings = []
|
||||
for level, check_id, pattern, message in CHECKS:
|
||||
for m in re.finditer(pattern, text):
|
||||
findings.append({
|
||||
"level": level,
|
||||
"id": check_id,
|
||||
"line": line_of(text, m.start()),
|
||||
"context": get_context(text, m),
|
||||
"message": message,
|
||||
})
|
||||
findings.sort(key=lambda f: (f["line"], f["id"]))
|
||||
return findings
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) > 1:
|
||||
text = " ".join(sys.argv[1:])
|
||||
elif not sys.stdin.isatty():
|
||||
text = sys.stdin.read()
|
||||
else:
|
||||
print("Usage: python lint-voice.py \"text\" OR pipe text via stdin", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
if not text.strip():
|
||||
print("✓ CLEAN — empty input.", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
|
||||
findings = run_checks(text)
|
||||
errors = [f for f in findings if f["level"] == "ERROR"]
|
||||
warns = [f for f in findings if f["level"] == "WARN"]
|
||||
|
||||
if not findings:
|
||||
print("✓ CLEAN — no issues found.")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"Found {len(findings)} issue(s): {len(errors)} ERROR, {len(warns)} WARN\n")
|
||||
|
||||
for f in findings:
|
||||
label = f"[{f['level']}]"
|
||||
action = "Fix" if f["level"] == "ERROR" else "Note"
|
||||
print(f"{label} {f['id']} — line {f['line']}")
|
||||
print(f' Context: "{f["context"]}"')
|
||||
print(f" {action}: {f['message']}")
|
||||
print()
|
||||
|
||||
print("---")
|
||||
if errors:
|
||||
print(f"EXIT 1 — {len(errors)} error(s) present. Fix and re-run.")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("EXIT 0 — no errors (warnings only). Review WARNs if applicable.")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -12,7 +12,7 @@ The visual format of every generated PDF must match `base/reference/Oleg_Proskur
|
|||
|
||||
## Writing texts as Oleg
|
||||
|
||||
Any English text that goes out under Oleg's name — cover letter, application essay, form answer, outreach DM, CV bullet — must be written with the **`write-as-oleg` skill** (`.claude/skills/write-as-oleg/`). The skill consolidates his voice profile, writing process, and a paired example dictionary, and runs a mandatory deterministic linter (`lint-voice.py`) before delivery. Do not write such texts without loading this skill first.
|
||||
Any English text that goes out under Oleg's name — cover letter, application essay, form answer, outreach DM, CV bullet — must be written with the **`write-as-oleg` skill** (`.claude/skills/write-as-oleg/`). The skill consolidates his voice profile, writing process, and a paired example dictionary, and runs a mandatory deterministic linter (`lint-voice.mjs`) before delivery. Do not write such texts without loading this skill first.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -282,6 +282,7 @@ The CSS in `templates/cv-style.css` already encodes these — when generating HT
|
|||
- **Language**: every artifact (HTML, MD, PDF, tracking rows, card notes) is in English. Always.
|
||||
- **Filenames**: snake_case for base CVs (`oleg_proskurin_<role>_cv.md`), kebab-case for tailored slugs. Tailored CV: `cv.md` inside `tailored/<slug>/`.
|
||||
- **Dates**: absolute (`2026-05-24`), never "last Thursday".
|
||||
- **Scripts**: all scripts in this project must be written in **Node.js** or **bash** — no Python. Use Node.js for anything with logic (parsing, transformation, linting, generation); use bash for simple shell glue (piping, file ops, invoking other tools).
|
||||
- **Don't edit the reference PDF.** It is the immutable visual anchor.
|
||||
- **Don't auto-regenerate PDFs** unless Oleg asks — show diffs first when content changes.
|
||||
- **Verify before claiming "done"**: after `pnpm pdf`, confirm the PDF exists and looks correct (open it, check page count). If you can't visually verify, say so.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue