Merge pull request #997 from alirezarezvani/claude/deep-learning-book-skill-xnbaz7

This commit is contained in:
Alireza Rezvani 2026-08-30 11:46:14 +02:00 • committed by GitHub
commit 527a1b829e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 539 additions and 35 deletions

View file

@ -225,6 +225,9 @@ def mine(rows: list, attributes: list) -> dict:
"multiple_comparisons_note": (
f"{len(tested)} candidate(s) reached the test at alpha {ALPHA}. On noise alone you "
f"would expect about {expected_false} to pass. {len(supported)} did. "
"This is an accounting of that expectation, not an applied correction: no "
"Bonferroni or Benjamini-Hochberg adjustment is made to the per-candidate "
"threshold, so a single passing candidate is weak evidence on its own. "
+ ("Treat these as hypotheses to test deliberately, not as conclusions."
if len(supported) <= max(1, expected_false)
else "More passed than chance predicts, which is mild evidence something real is "
@ -262,6 +265,25 @@ def render_human(r: dict) -> str:
return "\n".join(lines)
def _read_input(path: str) -> str:
"""Read --input, turning an unreadable path into a usage error, not a traceback.
Every script in this plugin caught a JSON decode error but let OSError escape, so
a mistyped path exited 1 with a FileNotFoundError stack instead of a typed code.
"""
if path == "-":
return sys.stdin.read()
try:
with open(path, encoding="utf-8") as handle:
return handle.read()
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError is a ValueError subclass, not an OSError, so a file that
# exists but is not valid UTF-8 escaped the OSError catch as a traceback -
# the same failure mode this helper exists to prevent for a bad path.
print(f"cannot read --input {path}: {err}", file=sys.stderr)
raise SystemExit(2)
def main() -> int:
ap = argparse.ArgumentParser(
description="Test candidate LinkedIn patterns against a permutation null "
@ -278,7 +300,7 @@ def main() -> int:
if args.sample:
rows = SAMPLE
elif args.input:
raw = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
raw = _read_input(args.input)
try:
rows = ([dict(r) for r in csv.DictReader(io.StringIO(raw))] if args.csv
else json.loads(raw))
@ -287,6 +309,10 @@ def main() -> int:
return 4
if isinstance(rows, dict):
rows = rows.get("posts") or rows.get("rows") or []
if not isinstance(rows, list) or any(not isinstance(r, dict) for r in rows):
print("ERROR: input must be a list of post objects (a bare list of values "
"cannot be mined).", file=sys.stderr)
return 4
else:
ap.error("--input or --sample is required")

View file

@ -95,6 +95,16 @@ def load_rows(raw: str, as_csv: bool) -> list:
data = json.loads(raw)
if isinstance(data, dict):
data = data.get("posts") or data.get("rows") or []
if not isinstance(data, list):
raise ValueError(f"expected a list of post objects, got {type(data).__name__}")
# A bare list of scalars ([1,2,3]) used to reach row.get() and die with an
# AttributeError traceback instead of a typed exit code.
bad = next((r for r in data if not isinstance(r, dict)), None)
if bad is not None:
raise ValueError(
f"every row must be an object with an 'impressions' field; found a "
f"{type(bad).__name__} ({bad!r:.40})"
)
return data
@ -142,9 +152,22 @@ def analyse(rows: list) -> dict:
q1, q3 = percentile(ers, 25), percentile(ers, 75)
iqr = q3 - q1
hi_fence, lo_fence = q3 + 1.5 * iqr, q1 - 1.5 * iqr
# With no dispersion (identical or heavily rounded rates) every fence collapses
# onto the median, and the >= hi_fence test below would label every post
# BREAKOUT - the opposite of describing the data honestly.
#
# But iqr == 0 only means the middle 50% is flat, which is a realistic shape for
# a real export: many similar posts plus a couple of viral ones. Collapsing
# everything to TYPICAL there hides genuine breakouts, trading one honesty
# problem for another. So when the centre is degenerate, rank against the median
# itself: equal to it is TYPICAL, above it is a real standout, below it is weak.
degenerate = iqr == 0
for p in clean:
e = p["engagement_rate"]
if degenerate:
p["band"] = "TYPICAL" if e == med else ("BREAKOUT" if e > med else "WEAK")
continue
if e >= hi_fence:
p["band"] = "BREAKOUT"
elif e >= q3:
@ -219,6 +242,25 @@ def render_human(r: dict) -> str:
return "\n".join(lines)
def _read_input(path: str) -> str:
"""Read --input, turning an unreadable path into a usage error, not a traceback.
Every script in this plugin caught a JSON decode error but let OSError escape, so
a mistyped path exited 1 with a FileNotFoundError stack instead of a typed code.
"""
if path == "-":
return sys.stdin.read()
try:
with open(path, encoding="utf-8") as handle:
return handle.read()
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError is a ValueError subclass, not an OSError, so a file that
# exists but is not valid UTF-8 escaped the OSError catch as a traceback -
# the same failure mode this helper exists to prevent for a bad path.
print(f"cannot read --input {path}: {err}", file=sys.stderr)
raise SystemExit(2)
def main() -> int:
ap = argparse.ArgumentParser(
description="Describe your own LinkedIn post export honestly "
@ -236,10 +278,12 @@ def main() -> int:
if args.sample:
rows = SAMPLE
elif args.input:
raw = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
raw = _read_input(args.input)
try:
rows = load_rows(raw, args.csv)
except (json.JSONDecodeError, csv.Error) as exc:
except (ValueError, csv.Error) as exc:
# ValueError covers json.JSONDecodeError (its subclass) and the explicit
# row-shape errors load_rows raises.
print(f"ERROR: could not parse input: {exc}", file=sys.stderr)
return 4
else:

View file

@ -295,6 +295,25 @@ def render_human(r: dict) -> str:
return "\n".join(lines)
def _read_input(path: str) -> str:
"""Read --input, turning an unreadable path into a usage error, not a traceback.
Every script in this plugin caught a JSON decode error but let OSError escape, so
a mistyped path exited 1 with a FileNotFoundError stack instead of a typed code.
"""
if path == "-":
return sys.stdin.read()
try:
with open(path, encoding="utf-8") as handle:
return handle.read()
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError is a ValueError subclass, not an OSError, so a file that
# exists but is not valid UTF-8 escaped the OSError catch as a traceback -
# the same failure mode this helper exists to prevent for a bad path.
print(f"cannot read --input {path}: {err}", file=sys.stderr)
raise SystemExit(2)
def main() -> int:
ap = argparse.ArgumentParser(
description="Lint a LinkedIn post 0-100 (SHIP=0 / REVISE=2 / REWRITE=3).")
@ -312,7 +331,7 @@ def main() -> int:
elif args.text:
text = args.text
elif args.input:
text = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
text = _read_input(args.input)
else:
ap.error("one of --text, --input, or --sample is required")

View file

@ -193,6 +193,25 @@ def load_ledger(path: str) -> dict:
return {}
def _read_input(path: str) -> str:
"""Read --input, turning an unreadable path into a usage error, not a traceback.
Every script in this plugin caught a JSON decode error but let OSError escape, so
a mistyped path exited 1 with a FileNotFoundError stack instead of a typed code.
"""
if path == "-":
return sys.stdin.read()
try:
with open(path, encoding="utf-8") as handle:
return handle.read()
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError is a ValueError subclass, not an OSError, so a file that
# exists but is not valid UTF-8 escaped the OSError catch as a traceback -
# the same failure mode this helper exists to prevent for a bad path.
print(f"cannot read --input {path}: {err}", file=sys.stderr)
raise SystemExit(2)
def main() -> int:
ap = argparse.ArgumentParser(
description="Split long source material into standalone LinkedIn units "
@ -214,7 +233,7 @@ def main() -> int:
if args.sample:
text = SAMPLE_SOURCE
elif args.input:
text = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
text = _read_input(args.input)
else:
ap.error("--input or --sample is required")

View file

@ -58,7 +58,23 @@ PITCH_RE = re.compile(
r"schedule a (call|demo)|are you the right person|decision[- ]maker|"
r"i'?d like to show you|free trial|pricing|proposal)\b", re.I)
ASK_RE = re.compile(r"\b(call|chat|meeting|demo|coffee|zoom|15 min|30 min|hop on|jump on)\b", re.I)
# There used to be a loose ASK_RE here (call|chat|meeting|demo|coffee|zoom|...). It
# was only ever safe against the --ask field, where every word is already an ask;
# against a whole note it matched "your post on on-call rotations". Both branches
# below scanned the whole note with it, in opposite directions: the connection branch
# raised a false premature-ask, and the else branch SUPPRESSED a real
# pitch-without-ask. This pattern requires meeting-request framing, so both can read
# the entire note - without which premature-ask was bypassable via --reason.
MEETING_ASK_RE = re.compile(
r"\b(hop|jump|get)\s+on\s+(a|an|the)?\s*(quick\s+)?(call|chat|zoom|meeting)\b"
r"|\b(grab|get)\s+(a\s+)?coffee\b"
r"|\b(book|schedule|set\s?up|arrange)\s+(a|an|some)?\s*(call|chat|meeting|demo|zoom|time)\b"
r"|\b\d{1,3}\s*(min|mins|minute|minutes)\b[^.]{0,20}\b(call|chat|zoom|meeting)\b"
r"|\b(quick|short|brief)\s+(call|chat|zoom|meeting)\b"
r"|\b(open to|free for|available for|would love|keen)\b[^.]{0,25}"
r"\b(call|chat|meeting|demo|coffee|zoom)\b",
re.I,
)
SAMPLE = {
"type": "connection",
@ -126,9 +142,12 @@ def validate(text: str, parts: dict, mtype: str, premium: bool) -> list:
"rate drops.",
"One specific line, one reason, one bounded ask. Everything else is for the reply.")
ask = (parts.get("ask") or "").strip()
if mtype == "connection":
ask = (parts.get("ask") or "").strip()
if ask:
# Read the assembled note, not just the --ask field: the same ask moved into
# --reason or --specific-line used to pass clean, which made the documented
# "refuses an ask in a first-touch note" guarantee bypassable.
if ask or MEETING_ASK_RE.search(low):
add("blocking", "premature-ask",
"A connection note carries an ask. The note is for getting into the room; the "
"ask belongs in the conversation after they accept.",
@ -141,7 +160,10 @@ def validate(text: str, parts: dict, mtype: str, premium: bool) -> list:
"Delete it. Nobody has ever bought from a connection request, and the request "
"is the only impression you get.")
else:
if PITCH_RE.search(low) and not ASK_RE.search(low):
# An ask is the --ask field, or meeting-request framing in the note. Matching
# loosely here silently lost the warning on any note that happened to contain
# "on-call", "chat feature" or "demo video". See MEETING_ASK_RE above.
if PITCH_RE.search(low) and not (ask or MEETING_ASK_RE.search(low)):
add("warning", "pitch-without-ask",
"Product language with no clear, bounded ask.",
"Either make the ask explicit and small, or cut the product language.")
@ -208,6 +230,25 @@ def render_human(r: dict) -> str:
return "\n".join(lines)
def _read_input(path: str) -> str:
"""Read --input, turning an unreadable path into a usage error, not a traceback.
Every script in this plugin caught a JSON decode error but let OSError escape, so
a mistyped path exited 1 with a FileNotFoundError stack instead of a typed code.
"""
if path == "-":
return sys.stdin.read()
try:
with open(path, encoding="utf-8") as handle:
return handle.read()
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError is a ValueError subclass, not an OSError, so a file that
# exists but is not valid UTF-8 escaped the OSError catch as a traceback -
# the same failure mode this helper exists to prevent for a bad path.
print(f"cannot read --input {path}: {err}", file=sys.stderr)
raise SystemExit(2)
def main() -> int:
ap = argparse.ArgumentParser(
description="Assemble one LinkedIn outreach message (PASS=0 / WARN=2 / FAIL=3). "
@ -231,7 +272,7 @@ def main() -> int:
if args.sample:
parts, mtype, premium = SAMPLE, SAMPLE["type"], SAMPLE["premium"]
elif args.input:
raw = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
raw = _read_input(args.input)
try:
parts = json.loads(raw)
except json.JSONDecodeError as exc:

View file

@ -222,6 +222,25 @@ def render_human(r: dict) -> str:
return "\n".join(lines)
def _read_input(path: str) -> str:
"""Read --input, turning an unreadable path into a usage error, not a traceback.
Every script in this plugin caught a JSON decode error but let OSError escape, so
a mistyped path exited 1 with a FileNotFoundError stack instead of a typed code.
"""
if path == "-":
return sys.stdin.read()
try:
with open(path, encoding="utf-8") as handle:
return handle.read()
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError is a ValueError subclass, not an OSError, so a file that
# exists but is not valid UTF-8 escaped the OSError catch as a traceback -
# the same failure mode this helper exists to prevent for a bad path.
print(f"cannot read --input {path}: {err}", file=sys.stderr)
raise SystemExit(2)
def main() -> int:
ap = argparse.ArgumentParser(
description="Assemble and validate a LinkedIn About section "
@ -247,7 +266,7 @@ def main() -> int:
if args.sample:
parts = SAMPLE
elif args.input:
raw = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
raw = _read_input(args.input)
try:
parts = json.loads(raw)
except json.JSONDecodeError as exc:

View file

@ -45,7 +45,10 @@ BUZZWORDS = [
# Words that signal an audience is being named.
AUDIENCE_MARKERS = [
"for ", "helping", "i help", "we help", "to ", "founders", "ctos", "cto",
# Bare "for " and "to " were removed: they matched ordinary prose ("excited
# to share...") while adding nothing, since a headline that names an audience
# already hits one of the nouns below.
"helping", "i help", "we help", "founders", "ctos", "cto",
"cmos", "engineers", "designers", "marketers", "recruiters", "startups",
"smbs", "smes", "enterprises", "teams", "b2b", "b2c", "saas", "agencies",
"nonprofits", "students", "clinicians", "operators", "pms", "product managers",
@ -57,7 +60,10 @@ OUTCOME_MARKERS = [
"ship", "grow", "scale", "reduce", "cut", "increase", "double", "win",
"hire", "raise", "launch", "fix", "unblock", "automate", "migrate",
"build", "turn", "convert", "retain", "save", "speed", "faster",
"without", "so they", "so you", "so that", "→", "->", "from ", "into ",
"without", "so they", "so you", "so that", "→", "->",
# Bare "from " and "into " matched any sentence containing them; the
# transformation shapes they were meant to catch are kept explicitly.
"from scratch", "from zero", "from manual",
]
# Terms recruiters and buyers actually type into LinkedIn search.
@ -76,6 +82,36 @@ SAMPLE_GOOD = ("Fractional Head of Data for Series A/B SaaS | Cut BigQuery spend
SAMPLE_WEAK = "Senior Software Engineer | Passionate about technology | Team player"
# Directed-at constructions. Bare "for " and "to " used to live in AUDIENCE_MARKERS
# and matched ordinary prose ("excited to share..."), but dropping them outright cost
# a real signal: "Head of Data for Series A/B SaaS" names an audience through the
# construction, not through a noun alone. These require "for"/"serving"/"helping" to
# actually land on an audience.
AUDIENCE_SHAPES = (
re.compile(r"\b(for|serving|helping)\s+[^|,.]{0,30}\b("
r"founders?|ctos?|cmos?|cios?|engineers?|designers?|marketers?|recruiters?|"
r"startups?|smbs?|smes?|enterprises?|teams?|b2b|b2c|saas|agencies|"
r"nonprofits?|students?|clinicians?|operators?|pms|product managers?|"
r"developers?|hr|investors?)\b", re.I),
re.compile(r"\bfor\s+(pre[- ]?)?(seed|series\s+[a-d](\s*/\s*[a-d])?)\b", re.I),
re.compile(r"\bfor\s+(mid[- ]market|enterprise|smb|early[- ]stage)\b", re.I),
)
# "from " kept idiomatic replacements ("from scratch"/"from zero"/"from manual")
# when the bare prepositions were removed; "into " had none, so a headline built on
# an explicit transformation lost the signal unless it also used turn/convert. This
# restores it as the construction itself rather than a bare preposition, so ordinary
# prose ("putting budget into growth") still scores nothing.
OUTCOME_SHAPES = (
re.compile(r"\bfrom\b[^|,.]{0,30}\b(to|into)\b", re.I),
)
def _find_shapes(text: str, shapes) -> list:
return [m.group(0).strip() for r in shapes for m in [r.search(text)] if m]
def _find(text_low: str, needles: list) -> list:
return [n for n in needles if n in text_low]
@ -106,7 +142,7 @@ def score_headline(text: str) -> dict:
findings, dims = [], {}
# --- AUDIENCE -----------------------------------------------------------
aud = _find(low, AUDIENCE_MARKERS)
aud = _find(low, AUDIENCE_MARKERS) + _find_shapes(raw, AUDIENCE_SHAPES)
dims["audience"] = 20 if len(aud) >= 2 else (12 if aud else 0)
if not aud:
findings.append({
@ -116,7 +152,7 @@ def score_headline(text: str) -> dict:
})
# --- OUTCOME ------------------------------------------------------------
out = _find(low, OUTCOME_MARKERS)
out = _find(low, OUTCOME_MARKERS) + _find_shapes(raw, OUTCOME_SHAPES)
dims["outcome"] = 20 if len(out) >= 2 else (12 if out else 0)
if not out:
findings.append({
@ -251,6 +287,25 @@ def render_human(r: dict) -> str:
return "\n".join(lines)
def _read_input(path: str) -> str:
"""Read --input, turning an unreadable path into a usage error, not a traceback.
Every script in this plugin caught a JSON decode error but let OSError escape, so
a mistyped path exited 1 with a FileNotFoundError stack instead of a typed code.
"""
if path == "-":
return sys.stdin.read()
try:
with open(path, encoding="utf-8") as handle:
return handle.read()
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError is a ValueError subclass, not an OSError, so a file that
# exists but is not valid UTF-8 escaped the OSError catch as a traceback -
# the same failure mode this helper exists to prevent for a bad path.
print(f"cannot read --input {path}: {err}", file=sys.stderr)
raise SystemExit(2)
def main() -> int:
ap = argparse.ArgumentParser(
description="Score a LinkedIn headline 0-100 (SHIP=0 / SHARPEN=2 / REWRITE=3).")
@ -271,7 +326,7 @@ def main() -> int:
elif args.headline:
text = args.headline
elif args.input:
text = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
text = _read_input(args.input)
else:
ap.error("one of --headline, --input, --sample, or --sample-weak is required")

View file

@ -31,6 +31,7 @@ Stdlib only. No network. Deterministic.
import argparse
import json
import re
import sys
# (key, weight, effort_hours, label)
@ -97,8 +98,33 @@ SAMPLE = {
"days_since_last_post": 210,
}
# Matched on word boundaries, never as substrings. As plain `w in low` tests, "led"
# credited "scheduled", "installed", "handled" and "recalled", and "cut" credited
# "executed" - so "Scheduled onboarding for new hires", a pure duty bullet, scored
# as outcome-carrying. That is the same substring defect that removing bare "to" /
# "from" / "x" from this tuple was meant to end; these two just survived it.
OUTCOME_WORDS = ("cut", "grew", "reduced", "increased", "shipped", "launched", "saved",
"doubled", "migrated", "led", "%", "x", "from", "to")
"doubled", "migrated", "led")
OUTCOME_WORD_RE = re.compile(r"\b(" + "|".join(OUTCOME_WORDS) + r")\b", re.I)
# Bare "x", "to" and "from" used to sit in OUTCOME_WORDS as loose substrings, which
# credited pure duty bullets: "Reported to the VP of Engineering" scored as
# outcome-carrying on the word "to". A multiplier and a from/to delta are genuine
# outcome signals, so they are kept - as shapes that require a number, not as
# substrings that match any prose.
OUTCOME_SHAPES = (
re.compile(r"\b\d+(\.\d+)?\s*x\b"), # 3x, 2.5x
re.compile(r"\bfrom\b[^.]{0,40}\bto\b[^.]{0,25}\d"), # from 4h to 20m
re.compile(r"\d+\s*%"), # 40%
)
def carries_outcome(bullet: str) -> bool:
"""True when a bullet claims a result rather than a responsibility."""
low = bullet.lower()
if OUTCOME_WORD_RE.search(low):
return True
return any(r.search(low) for r in OUTCOME_SHAPES)
def evaluate_check(key: str, p: dict) -> tuple:
@ -130,8 +156,7 @@ def evaluate_check(key: str, p: dict) -> tuple:
return 0.0, "no current role listed"
if not bullets:
return 0.3, "role listed with no description"
with_outcome = [b for b in bullets
if any(w in b.lower() for w in OUTCOME_WORDS)]
with_outcome = [b for b in bullets if carries_outcome(b)]
frac = 0.4 + 0.6 * (len(with_outcome) / max(1, len(bullets)))
return min(1.0, frac), f"{len(with_outcome)}/{len(bullets)} bullets carry an outcome"
if key == "featured":
@ -232,6 +257,25 @@ def render_human(r: dict) -> str:
return "\n".join(lines)
def _read_input(path: str) -> str:
"""Read --input, turning an unreadable path into a usage error, not a traceback.
Every script in this plugin caught a JSON decode error but let OSError escape, so
a mistyped path exited 1 with a FileNotFoundError stack instead of a typed code.
"""
if path == "-":
return sys.stdin.read()
try:
with open(path, encoding="utf-8") as handle:
return handle.read()
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError is a ValueError subclass, not an OSError, so a file that
# exists but is not valid UTF-8 escaped the OSError catch as a traceback -
# the same failure mode this helper exists to prevent for a bad path.
print(f"cannot read --input {path}: {err}", file=sys.stderr)
raise SystemExit(2)
def main() -> int:
ap = argparse.ArgumentParser(
description="Audit a LinkedIn profile 0-100 and rank fixes by points per hour "
@ -251,7 +295,7 @@ def main() -> int:
if args.sample:
profile = SAMPLE
elif args.input:
raw = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
raw = _read_input(args.input)
try:
profile = json.loads(raw)
except json.JSONDecodeError as exc:

View file

@ -110,6 +110,25 @@ def decide(scores: dict) -> dict:
return {"decision": "ASK", "candidates": candidates, "exit": 2}
def _read_input(path: str) -> str:
"""Read --input, turning an unreadable path into a usage error, not a traceback.
Every script in this plugin caught a JSON decode error but let OSError escape, so
a mistyped path exited 1 with a FileNotFoundError stack instead of a typed code.
"""
if path == "-":
return sys.stdin.read()
try:
with open(path, encoding="utf-8") as handle:
return handle.read()
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError is a ValueError subclass, not an OSError, so a file that
# exists but is not valid UTF-8 escaped the OSError catch as a traceback -
# the same failure mode this helper exists to prevent for a bad path.
print(f"cannot read --input {path}: {err}", file=sys.stderr)
raise SystemExit(2)
def main() -> int:
ap = argparse.ArgumentParser(
description="Deterministic lane router for LinkedIn goals "
@ -127,7 +146,7 @@ def main() -> int:
elif args.text:
text = args.text
elif args.input:
text = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
text = _read_input(args.input)
else:
ap.error("one of --text, --input, or --sample is required")

View file

@ -47,6 +47,35 @@ REFUSE_RULES = [
r"\bbrowser (extension|plugin|add-?on)\b.{0,40}\b(linkedin|connect|message)\b",
r"\bscript that (logs? in|clicks?|sends?|connects?)\b",
],
"exemptions": [
{
# P7's own substitute names native LinkedIn scheduling as the supported
# path, so refusing "auto-post with LinkedIn's scheduler" contradicted
# this gate's own advice. The exemption covers POSTING only: LinkedIn's
# scheduler publishes posts, it does not connect, message, like or follow,
# so those matches still refuse even in the same sentence.
"signal": r"\b(linkedin'?s?\s+)?(own|native|built[-\s]?in)\b[^.]{0,40}\bschedul\w*"
r"|\bnative\b[^.]{0,20}\bschedul\w*"
r"|\blinkedin'?s?\s+schedul\w*",
"excuses": r"^auto[-\s]?(post|publish|schedul)\w*$",
"why": "LinkedIn's own scheduling feature — the supported path named in P7.",
},
{
# Same endorsed action reached through the other P1 pattern:
# "automate posting with LinkedIn's native scheduler" matches
# \bautomat(e|ed|ing|ion)\b and yields the bare snippet "automate".
# That token is far too generic to exempt on the signal alone - "use
# the native scheduler and automate my DMs" would sail through - so
# this one also requires posting language immediately after the
# matched word. "automate my DMs" keeps refusing.
"signal": r"\b(linkedin'?s?\s+)?(own|native|built[-\s]?in)\b[^.]{0,40}\bschedul\w*"
r"|\bnative\b[^.]{0,20}\bschedul\w*"
r"|\blinkedin'?s?\s+schedul\w*",
"excuses": r"^automat(e|es|ed|ing|ion)$",
"context": r"\b(post|posts|posting|publish\w*|content|updates?)\b",
"why": "LinkedIn's own scheduling feature, applied to posting.",
},
],
"substitute": "Do the same volume by hand on a capped schedule. "
"`linkedin-engagement/scripts/outreach_volume_guard.py` sizes a manual "
"cadence you can actually sustain; the plugin drafts the text, you press send.",
@ -63,6 +92,23 @@ REFUSE_RULES = [
r"\bemail finder\b", r"\bfind (their|his|her) email\b",
r"\bbuild(ing)? a (lead )?(list|database)\b.{0,30}\bfrom linkedin\b",
],
"exemptions": [
{
# This rule's own substitute tells the user to run LinkedIn's export of
# their own data, so refusing "export my connections list as a csv" told
# them not to do the thing it recommends. Scoped tightly: only an EXPORT
# match, only of the user's own connections/contacts/network/data. A
# leads or member-profile database is not the self-export and still
# refuses, as does any scrape/crawl/harvest match in the same text.
"signal": r"\b(my|our|your)\s+own\b"
r"|\bget a copy of (my|our|your) data\b"
r"|\bdata privacy\b"
r"|\bexport\b[^.]{0,20}\b(my|our|your)\b",
"excuses": r"^export\b.*\b(connections?|contacts?|network|data)\b",
"why": "LinkedIn's own export of your own data — the substitute this rule "
"recommends.",
},
],
"substitute": "Use LinkedIn's own export of YOUR data (Settings → Data privacy → "
"Get a copy of your data) and LinkedIn-native search. Analytics work in "
"this plugin runs on your own exported post/profile stats, never on "
@ -75,6 +121,9 @@ REFUSE_RULES = [
"Community Policies (be authentic / no fake engagement)",
"patterns": [
r"\b(engagement|comment|like|linkedin) ?pod\b", r"\bpods?\b(?=.{0,30}\b(join|run|group)\b)",
# The lookahead above only fires noun-before-verb ("pods to join"); the common
# phrasing is verb-first ("join a pod"), which it structurally cannot match.
r"\b(join|joining|run|running|start|starting|invite[sd]? me to)\b.{0,20}\bpods?\b",
r"\bbuy(ing)? (followers?|likes?|comments?|connections?|views?|impressions?)\b",
r"\b(fake|paid|bought|purchased) (followers?|engagement|likes?|comments?)\b",
r"\bengagement (group|ring|circle|exchange|swap)\b",
@ -104,7 +153,7 @@ REFUSE_RULES = [
},
{
"id": "P5-BULK-MESSAGING",
"title": "Bulk or unsolicited mass messaging",
"title": "Bulk or unsolicited mass messaging or posting",
"anchor": "LinkedIn User Agreement §8.2 (send or redirect messages by automated means; "
"spam) + Professional Community Policies (no spam/unsolicited commercial content)",
"patterns": [
@ -113,10 +162,23 @@ REFUSE_RULES = [
r"\bsend the same (message|dm|note) to\b",
r"\bcopy[- ]?paste\b.{0,25}\b(dm|message|outreach)\b.{0,25}\b(everyone|all|hundreds)\b",
r"\bdrip (campaign|sequence)\b.{0,30}\blinkedin\b",
# Unsolicited bulk POSTING, not just messaging. Before the P1 exemption
# existed, "auto-post to 50 groups without permission" was refused only
# incidentally, because P1 refused every automation word. Now that native
# scheduling is correctly allowed, the spam itself needs its own rule -
# otherwise fixing the self-contradiction quietly permitted the spam.
r"\bpost\w*\b[^.]{0,40}\b(to|in|into|across)\b[^.]{0,20}\b\d{2,}\b[^.]{0,20}"
r"\b(groups?|communities|forums?|subreddits?)\b",
r"\b(spam|blast|dump)\w*\b[^.]{0,30}\b(groups?|communities|feeds?)\b",
r"\bwithout permission\b[^.]{0,40}\b(post|group|share|promot)\w*"
r"|\b(post|group|share|promot)\w*[^.]{0,40}\bwithout permission\b",
],
"substitute": "Per-person messages with a specific reason, sent by hand, under a weekly "
"cap. `outreach_message_builder.py` refuses a template with no "
"person-specific line for exactly this reason.",
"person-specific line for exactly this reason. For groups: post where "
"you are a participating member, on a cadence "
"`cadence_planner.py` says you can sustain, and only where the group's "
"own rules allow it.",
},
{
"id": "P6-FABRICATION",
@ -215,30 +277,137 @@ SAMPLE_TEXT = ("I want to grow to 20k followers in six months. Plan: use Dux-Sou
"90 minutes, and blast the same DM to everyone who accepts.")
def _scan(text: str, rules: list, key: str) -> list:
def _scan(text: str, rules: list, key: str, exemptions: list) -> list:
"""Match rules against text, dropping matches an exemption explains.
A rule may carry exemptions so it does not refuse the very substitute it
recommends. An exemption drops a single matched snippet — never the whole rule
— so a sentence that mixes an endorsed action with a prohibited one still
refuses on the prohibited part.
"""
low = text.lower()
hits = []
for rule in rules:
matched = []
matched, excused = [], []
# excused holds dicts, so a `snippet in excused` membership test compares a
# str against dicts and is always False - the same phrase appearing twice was
# re-evaluated and reported twice, making exemptions_applied overstate what
# was found. Track the snippet strings separately.
excused_snippets: set = set()
for pat in rule["patterns"]:
for m in re.finditer(pat, low, re.IGNORECASE):
snippet = m.group(0).strip()
if snippet and snippet not in matched:
if not snippet:
continue
# Every OCCURRENCE is judged on its own position. De-duplicating by
# snippet text before this call silently dropped the second use of a
# generic word: "automate posting via the native scheduler, and also
# automate direct messaging to my whole network" excused the first
# "automate", then skipped the second as a duplicate, so the mass-DM
# half was never evaluated and the whole request returned ALLOW.
# De-duplication is for the display lists only, below.
reason = _exemption_for(snippet, low, rule.get("exemptions", ()), m.start())
if reason:
if snippet not in excused_snippets:
excused_snippets.add(snippet)
excused.append({"id": rule["id"], "snippet": snippet, "why": reason})
elif snippet not in matched:
matched.append(snippet)
# Record exemptions whether or not the rule still fires. Attaching them only
# to a surviving hit hid them in exactly the case that matters most: when
# every match is exempted the rule produces no hit at all, so an ALLOW that
# recognized and excused a prohibited-looking phrase looked identical to one
# that never matched anything.
exemptions.extend(excused)
if matched:
hits.append({
hit = {
"id": rule["id"],
"title": rule["title"],
"matched": matched[:5],
"anchor": rule.get("anchor", ""),
key: rule[key],
})
}
if excused:
hit["exempted"] = [e["snippet"] for e in excused][:5]
hits.append(hit)
return hits
_SENTENCE_SPLIT = re.compile(r"[.!?;\n]")
def _sentence_around(text: str, at: int) -> str:
"""Return the sentence containing offset `at`.
The signal that makes a match legitimate has to be about THIS action. Searching
the whole input let an unrelated aside excuse a match elsewhere: "auto-post
daily to 50 groups without permission... LinkedIn's native scheduler is neat,
right?" was allowed, because the endorsement in the second sentence excused the
spam in the first. That turned a refusal into an ALLOW, which is the one
direction this gate must never move by accident.
"""
start = 0
for m in _SENTENCE_SPLIT.finditer(text, 0, at):
start = m.end()
end_match = _SENTENCE_SPLIT.search(text, at)
end = end_match.start() if end_match else len(text)
return text[start:end]
_CLAUSE_BREAK = re.compile(
r"[.!?;,\n]|\b(and|but|then|also|plus|while|or|as well as)\b", re.IGNORECASE)
def _clause_after(text: str, at: int, span: int, cap: int = 40) -> str:
"""Return the text just after a match, cut at the first clause boundary.
`context` exists to require a word IMMEDIATELY after the matched token. A fixed
character window is not "immediately": it reaches into the next clause, so the
context belonging to a LATER occurrence could excuse an earlier, unrelated one.
"automate connects and automate posting via native scheduler" allowed the whole
request, because the second clause's "posting" fell inside the first
"automate"'s 40-char window - and adding a period in place of "and" refused it.
A gate whose verdict turns on incidental phrasing length is not a gate.
Both bounds apply, whichever is tighter: the clause boundary keeps another
clause's words out, and the character cap keeps one long unpunctuated clause
from drifting ("automate connecting with recruiters for my posting campaign").
"""
window = text[at + span:at + span + cap]
brk = _CLAUSE_BREAK.search(window)
return window[:brk.start()] if brk else window
def _exemption_for(snippet: str, text: str, exemptions, at: int = 0) -> str:
"""Return the reason this snippet is exempt, or "" if it is not.
Two gates always apply: the snippet must be one this exemption can excuse, and
the signal that makes it legitimate must appear IN THE SAME SENTENCE as the
match. An exemption may add a third, `context`, which must appear just after the
matched word - needed where the snippet itself is too generic to exempt safely.
Sentence scoping costs a false refusal on a legitimate two-sentence phrasing
("I use LinkedIn's native scheduler. I auto-post weekly."), which the user can
rephrase. The alternative cost is a false ALLOW on a prohibited tactic, which
they cannot detect at all.
"""
scope = _sentence_around(text, at)
for ex in exemptions:
if not re.search(ex["excuses"], snippet, re.IGNORECASE):
continue
if not re.search(ex["signal"], scope, re.IGNORECASE):
continue
ctx = ex.get("context")
if ctx and not re.search(ctx, _clause_after(text, at, len(snippet)), re.IGNORECASE):
continue
return ex["why"]
return ""
def evaluate(text: str) -> dict:
refusals = _scan(text, REFUSE_RULES, "substitute")
constraints = _scan(text, CONSTRAIN_RULES, "constraint")
exemptions: list = []
refusals = _scan(text, REFUSE_RULES, "substitute", exemptions)
constraints = _scan(text, CONSTRAIN_RULES, "constraint", exemptions)
if refusals:
verdict, code = "REFUSE", 4
elif constraints:
@ -250,6 +419,7 @@ def evaluate(text: str) -> dict:
"exit_code": code,
"refusals": refusals,
"constraints": constraints,
"exemptions_applied": exemptions,
"input_preview": text.strip()[:280],
"standing_constraints": STANDING_CONSTRAINTS,
"disclaimer": ("Deterministic pattern check against LinkedIn's published policies, not "
@ -273,6 +443,10 @@ def render_human(result: dict) -> str:
out.append(f" [{c['id']}] {c['title']}")
out.append(f" matched : {', '.join(c['matched'])}")
out.append(f" honor : {c['constraint']}\n")
if result.get("exemptions_applied"):
out.append("\nRecognized but exempt — these look like rule matches and are not:\n")
for e in result["exemptions_applied"]:
out.append(f" [{e['id']}] '{e['snippet']}' — {e['why']}\n")
if result["verdict"] == "ALLOW":
out.append("\nNothing in this request trips a known rule. Proceed.\n")
out.append("\nStanding constraints (always apply):")
@ -283,6 +457,25 @@ def render_human(result: dict) -> str:
return "\n".join(out)
def _read_input(path: str) -> str:
"""Read --input, turning an unreadable path into a usage error, not a traceback.
Every script in this plugin caught a JSON decode error but let OSError escape, so
a mistyped path exited 1 with a FileNotFoundError stack instead of a typed code.
"""
if path == "-":
return sys.stdin.read()
try:
with open(path, encoding="utf-8") as handle:
return handle.read()
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError is a ValueError subclass, not an OSError, so a file that
# exists but is not valid UTF-8 escaped the OSError catch as a traceback -
# the same failure mode this helper exists to prevent for a bad path.
print(f"cannot read --input {path}: {err}", file=sys.stderr)
raise SystemExit(2)
def main() -> int:
ap = argparse.ArgumentParser(
description="Classify a LinkedIn tactic against the User Agreement: "
@ -300,7 +493,7 @@ def main() -> int:
elif args.text:
text = args.text
elif args.input:
text = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
text = _read_input(args.input)
else:
ap.error("one of --text, --input, or --sample is required")

View file

@ -18,7 +18,8 @@ available.
Exit codes:
0 plan fits the budget
2 budget is below the floor — a comment-only week is returned instead
3 the requested target does not fit; the overage is named
3 the plan does not fit: either the requested target overruns the budget, or the
chosen format costs more than the budget allows for even one post
Stdlib only. No network. Deterministic.
"""
@ -134,8 +135,12 @@ def plan(minutes: int, stage: str, target_posts: int, formats: list,
affordable = target_posts
if affordable == 0 and verdict == "FITS":
# A week with zero posts is not a cadence that fits — it is a budget that
# cannot buy the chosen format. Reporting FITS/0 here told the caller the
# plan was fine while handing them a plan with nothing in it.
verdict, code = "NO_POSTS_AFFORDABLE", 3
findings.append({
"severity": "warning", "area": "capacity",
"severity": "blocking", "area": "capacity",
"finding": f"The chosen formats ({', '.join(chosen)}) cost more than the "
f"{creation_budget}-min creation budget allows for even one post.",
"fix": "Add text-post to the format mix, or accept a fortnightly cadence for the "
@ -220,7 +225,8 @@ def render_human(r: dict) -> str:
def main() -> int:
ap = argparse.ArgumentParser(
description="Size a sustainable LinkedIn week (fits=0 / below-floor=2 / over-budget=3).")
description="Size a sustainable LinkedIn week "
"(fits=0 / below-floor=2 / over-budget or no-posts-affordable=3).")
ap.add_argument("--minutes", type=int, help="Minutes per week you will actually protect.")
ap.add_argument("--stage", choices=sorted(STAGES), default="starting")
ap.add_argument("--target-posts", type=int, default=0,

View file

@ -246,6 +246,25 @@ def render_human(r: dict) -> str:
return "\n".join(lines)
def _read_input(path: str) -> str:
"""Read --input, turning an unreadable path into a usage error, not a traceback.
Every script in this plugin caught a JSON decode error but let OSError escape, so
a mistyped path exited 1 with a FileNotFoundError stack instead of a typed code.
"""
if path == "-":
return sys.stdin.read()
try:
with open(path, encoding="utf-8") as handle:
return handle.read()
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError is a ValueError subclass, not an OSError, so a file that
# exists but is not valid UTF-8 escaped the OSError catch as a traceback -
# the same failure mode this helper exists to prevent for a bad path.
print(f"cannot read --input {path}: {err}", file=sys.stderr)
raise SystemExit(2)
def main() -> int:
ap = argparse.ArgumentParser(
description="Validate a LinkedIn positioning brief "
@ -268,7 +287,7 @@ def main() -> int:
if args.sample:
brief = SAMPLE
elif args.input:
raw = sys.stdin.read() if args.input == "-" else open(args.input, encoding="utf-8").read()
raw = _read_input(args.input)
try:
brief = json.loads(raw)
except json.JSONDecodeError as exc: