fix: correct "bugs" to "issues" in routing table and README references across multiple documentation files

This commit is contained in:
2026-06-14 07:48:10 +00:00
parent f79a427f32
commit f2b910ea75
162 changed files with 7011 additions and 1134 deletions
+39 -46
View File
@@ -17,7 +17,6 @@ from devplacepy.services.bot.config import (
COMMENT_COOLDOWN_MIN_SECONDS,
FEED_TOPICS,
GIST_LANGUAGES,
HANDLE_SUFFIX_WORDS,
MAX_SIBLING_COMMENTS,
MAX_THREAD_REPLIES,
MENTION_REPLIES_PER_SESSION,
@@ -36,6 +35,7 @@ from devplacepy.services.bot.config import (
persona_article_score,
pick_category,
)
from devplacepy.services.bot.handles import make_handle
from devplacepy.services.bot.llm import LLMClient
from devplacepy.services.bot.news_fetcher import NewsFetcher
from devplacepy.services.bot.registry import ArticleRegistry
@@ -91,7 +91,7 @@ class DevPlaceBot:
self._session_mention_replies = 0
self._post_cache: list[tuple[str, str, str, str, str]] = []
self._project_cache: list[tuple[str, str]] = []
self._bug_cache: list[tuple[str, str]] = []
self._issue_cache: list[tuple[str, str]] = []
self._session_start_cost: float = 0.0
self._session_start_calls: int = 0
self._session_history: list[str] = []
@@ -214,10 +214,10 @@ class DevPlaceBot:
self.llm.generate_project_desc, t, self.state.persona
)
self._project_cache.append((t, desc))
elif kind == "bug":
elif kind == "issue":
topic = random.choice(["UI glitch", "Broken link", "Performance issue"])
bug = await asyncio.to_thread(self.llm.generate_bug, topic)
self._bug_cache.append(bug)
issue = await asyncio.to_thread(self.llm.generate_issue, topic)
self._issue_cache.append(issue)
except Exception as e:
logger.warning("Cache refill (%s) failed: %s", kind, e)
@@ -225,36 +225,33 @@ class DevPlaceBot:
self._log("Warming content cache (background)...")
self._post_cache = []
self._project_cache = []
self._bug_cache = []
self._issue_cache = []
self.news.fetch()
asyncio.create_task(self._refill_cache_async("post"))
asyncio.create_task(self._refill_cache_async("project"))
asyncio.create_task(self._refill_cache_async("bug"))
asyncio.create_task(self._refill_cache_async("issue"))
def _generate_handle(self) -> str:
first = re.sub(r"[^a-z]", "", self.faker.first_name().lower()) or "dev"
last = re.sub(r"[^a-z]", "", self.faker.last_name().lower()) or "coder"
styles = [
first,
f"{first}.{last}",
f"{first}{last}",
f"{first}_{last}",
f"{first[0]}{last}",
f"{first}{last[0]}",
f"{last}{first[0]}",
]
handle = random.choice(styles)
if random.random() < 0.18:
word = random.choice(HANDLE_SUFFIX_WORDS)
sep = random.choice(["", "_", ".", "-"])
handle = f"{handle}{sep}{word}"
handle = re.sub(r"[^a-z0-9._-]", "", handle)[:20]
return handle or "dev"
return make_handle(SEARCH_TERMS.get(self.state.persona, []))
def _next_handle(self) -> str:
while self.state.handle_candidates:
candidate = self.state.handle_candidates.pop(0)
if candidate:
return candidate
return self._generate_handle()
async def _ensure_auth(self) -> bool:
b = self.b
if not self.state.username:
self.state.username = self._generate_handle()
if not self.state.handle_candidates:
self.state.handle_candidates = (
await self._generate(
self.llm.generate_handle_candidates, self.state.persona
)
or []
)
self.state.username = self._next_handle()
self.state.email = self.faker.email()
self.state.password = self.faker.password(length=random.randint(12, 18))
self.state.started_at = datetime.now().isoformat()
@@ -299,14 +296,10 @@ class DevPlaceBot:
self._log(f"Registered as {self.state.username}")
return True
if attempt >= 2:
base = self._generate_handle()
word = random.choice(HANDLE_SUFFIX_WORDS)
sep = random.choice(["", "_", ".", "-"])
self.state.username = re.sub(
r"[^a-z0-9._-]", "", f"{base}{sep}{word}"
)[:20]
base = self._next_handle()
self.state.username = f"{base}{random.randint(10, 9999)}"[:20]
else:
self.state.username = self._generate_handle()
self.state.username = self._next_handle()
self.state.email = self.faker.email()
self._log(f"Signup failed, retrying as {self.state.username}")
return False
@@ -1794,15 +1787,15 @@ class DevPlaceBot:
return True
return False
async def _report_bug(self) -> bool:
async def _report_issue(self) -> bool:
b = self.b
await b.goto(f"{self.base_url}/bugs")
ok = await b.click("[data-modal='create-bug-modal']")
await b.goto(f"{self.base_url}/issues")
ok = await b.click("[data-modal='create-issue-modal']")
if not ok:
return False
await b._idle(0.5, 1.5)
if not await b.text("#create-bug-modal"):
if not await b.text("#create-issue-modal"):
return False
topic = random.choice(
@@ -1814,27 +1807,27 @@ class DevPlaceBot:
"Formatting error",
]
)
if self._bug_cache:
title, desc = self._bug_cache.pop(0)
asyncio.create_task(self._refill_cache_async("bug"))
if self._issue_cache:
title, desc = self._issue_cache.pop(0)
asyncio.create_task(self._refill_cache_async("issue"))
else:
generated = await self._generate(self.llm.generate_bug, topic)
generated = await self._generate(self.llm.generate_issue, topic)
if not generated:
self._log("Report bug: generation failed")
self._log("Report issue: generation failed")
return False
title, desc = generated
await b.fill("#bug-title", title[:200])
await b.fill("#issue-title", title[:200])
await b._idle(0.2, 0.5)
await b.fill("textarea[name='description']", desc[:2000])
await b._idle(0.5, 1.0)
ok = await b.click(
"button:has-text('Submit Report'), #create-bug-modal .btn-primary"
"button:has-text('Submit Report'), #create-issue-modal .btn-primary"
)
if ok:
self.state.bugs_filed += 1
self._action("BUG", f"{title[:50]} [{self.state.bugs_filed}]")
self.state.issues_filed += 1
self._action("ISSUE", f"{title[:50]} [{self.state.issues_filed}]")
await asyncio.sleep(2)
return True
return False
+137
View File
@@ -0,0 +1,137 @@
# retoor <retoor@molodetz.nl>
import random
import re
from typing import Optional
from devplacepy.services.bot.config import HANDLE_SUFFIX_WORDS
MIN_HANDLE_LEN = 3
MAX_HANDLE_LEN = 20
MIN_SEED_TOKEN_LEN = 3
LEET_PROBABILITY = 0.35
LEET_CHAR_PROBABILITY = 0.5
NUMBER_SUFFIX_PROBABILITY = 0.30
SEPARATOR_PROBABILITY = 0.30
CAPITALIZE_PROBABILITY = 0.22
ALLCAPS_PROBABILITY = 0.08
ALLCAPS_MAX_LEN = 8
PAD_NUMBER_FLOOR = 10
PAD_NUMBER_CEIL = 9999
LEET_MAP = {
"a": "4",
"e": "3",
"i": "1",
"o": "0",
"s": "5",
"t": "7",
"l": "1",
"g": "9",
"b": "8",
}
TECH_NOUNS = [
"byte", "bit", "nibble", "null", "void", "root", "kernel", "daemon",
"socket", "buffer", "cache", "stack", "heap", "pointer", "segfault",
"regex", "lambda", "vector", "packet", "proxy", "token", "hash", "cipher",
"vortex", "flux", "glitch", "pixel", "neon", "cyber", "quantum", "thread",
"mutex", "syntax", "binary", "octet", "raster", "shader", "codec",
"vertex", "matrix", "scalar", "payload", "opcode", "runtime", "compiler",
"parser", "lexer", "sandbox", "firmware", "bytecode", "checksum",
"entropy", "signal", "overflow", "nibbler", "cursor", "patch",
]
HANDLE_ADJECTIVES = [
"dark", "mad", "lazy", "quantum", "cyber", "neon", "ghost", "hyper",
"ultra", "neo", "retro", "raw", "zero", "async", "blind", "silent",
"atomic", "fatal", "rogue", "idle", "stale", "dirty", "lucid", "prime",
"hex", "cold", "lone", "wired", "broken", "feral",
]
HANDLE_CREATURES = [
"fox", "wolf", "raven", "owl", "octopus", "gecko", "mantis", "viper",
"lynx", "moth", "hydra", "kraken", "phoenix", "dragon", "falcon",
"panther", "cobra", "badger", "otter", "sloth", "hornet", "beetle",
"lemur", "narwhal", "basilisk",
]
HANDLE_PREFIXES = ["the", "mr", "dr", "lord", "captain", "uncle", "sir", "prof", "agent"]
NUMBER_SUFFIXES = ["42", "1337", "404", "64", "99", "007", "23", "88", "256", "512", "101", "777"]
SEPARATORS = ["_", "-"]
def _join(rng: random.Random, parts: list[str]) -> str:
separator = rng.choice(SEPARATORS) if rng.random() < SEPARATOR_PROBABILITY else ""
return separator.join(parts)
def _seed_tokens(interests: list[str]) -> list[str]:
tokens: list[str] = []
for term in interests:
for piece in re.split(r"[^a-z0-9]+", term.lower()):
if len(piece) >= MIN_SEED_TOKEN_LEN:
tokens.append(piece)
return tokens
def _build_core(rng: random.Random, interests: list[str]) -> str:
seeds = _seed_tokens(interests)
builders = [
lambda: rng.choice(TECH_NOUNS),
lambda: _join(rng, [rng.choice(HANDLE_ADJECTIVES), rng.choice(TECH_NOUNS)]),
lambda: _join(rng, [rng.choice(TECH_NOUNS), rng.choice(HANDLE_CREATURES)]),
lambda: _join(rng, [rng.choice(HANDLE_ADJECTIVES), rng.choice(HANDLE_CREATURES)]),
lambda: _join(rng, [rng.choice(HANDLE_PREFIXES), rng.choice(TECH_NOUNS + HANDLE_CREATURES)]),
lambda: _join(rng, [rng.choice(TECH_NOUNS), rng.choice(HANDLE_SUFFIX_WORDS)]),
]
weights = [3, 4, 3, 2, 2, 2]
if seeds:
builders.append(
lambda: _join(rng, [rng.choice(seeds), rng.choice(HANDLE_SUFFIX_WORDS + TECH_NOUNS)])
)
builders.append(lambda: _join(rng, [rng.choice(HANDLE_ADJECTIVES), rng.choice(seeds)]))
weights += [5, 3]
return rng.choices(builders, weights=weights)[0]()
def apply_leet(text: str, rng: random.Random) -> str:
chars: list[str] = []
for char in text:
replacement = LEET_MAP.get(char.lower())
if replacement and rng.random() < LEET_CHAR_PROBABILITY:
chars.append(replacement)
else:
chars.append(char)
return "".join(chars)
def _apply_case(text: str, rng: random.Random) -> str:
roll = rng.random()
if roll < ALLCAPS_PROBABILITY and len(text) <= ALLCAPS_MAX_LEN:
return text.upper()
if roll < ALLCAPS_PROBABILITY + CAPITALIZE_PROBABILITY:
return text[:1].upper() + text[1:]
return text
def sanitize_handle(raw: str) -> str:
handle = re.sub(r"[^a-zA-Z0-9_-]", "", str(raw).strip())
handle = re.sub(r"[_-]{2,}", lambda match: match.group(0)[0], handle).strip("_-")
return handle[:MAX_HANDLE_LEN].rstrip("_-")
def make_handle(interests: Optional[list[str]] = None, rng: Optional[random.Random] = None) -> str:
generator = rng or random
core = _build_core(generator, interests or [])
if generator.random() < LEET_PROBABILITY:
core = apply_leet(core, generator)
if generator.random() < NUMBER_SUFFIX_PROBABILITY:
core = f"{core}{generator.choice(NUMBER_SUFFIXES)}"
handle = sanitize_handle(core)
if len(handle) < MIN_HANDLE_LEN:
handle = f"{handle}{generator.randint(PAD_NUMBER_FLOOR, PAD_NUMBER_CEIL)}"
handle = _apply_case(handle, generator)
return handle or f"dev{generator.randint(PAD_NUMBER_FLOOR, PAD_NUMBER_CEIL)}"
+52 -21
View File
@@ -5,10 +5,11 @@ import logging
import random
import re
import time
import urllib.error
import urllib.request
from typing import Any
import httpx
from devplacepy import stealth
from devplacepy.services.bot.config import (
GIST_LANGUAGES,
GIST_MIN_LINES,
@@ -17,6 +18,9 @@ from devplacepy.services.bot.config import (
SEARCH_TERMS,
TRIVIAL_GIST_TERMS,
)
from devplacepy.services.bot.handles import MAX_HANDLE_LEN, sanitize_handle
HANDLE_CANDIDATE_TARGET = 8
logger = logging.getLogger(__name__)
@@ -61,19 +65,15 @@ class LLMClient:
json.dumps(system[:500]),
json.dumps(prompt[:500]),
)
data = json.dumps(payload).encode()
req = urllib.request.Request(
self.api_url,
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
},
)
with urllib.request.urlopen(req, timeout=30) as resp:
raw = resp.read()
with stealth.stealth_sync_client(timeout=30) as client:
resp = client.post(
self.api_url,
json=payload,
headers={"Authorization": f"Bearer {self.api_key}"},
)
raw = resp.content
logger.info("LLM <<< %s", raw[:2000].decode(errors="replace"))
result = json.loads(raw)
result = resp.json()
usage = result.get("usage", {})
in_tokens = usage.get("prompt_tokens", 0)
out_tokens = usage.get("completion_tokens", 0)
@@ -86,8 +86,7 @@ class LLMClient:
self.total_out_tokens += out_tokens
return result["choices"][0]["message"]["content"]
except (
urllib.error.HTTPError,
urllib.error.URLError,
httpx.HTTPError,
json.JSONDecodeError,
KeyError,
) as e:
@@ -126,7 +125,7 @@ class LLMClient:
"snippet",
"headline",
"gist",
"bug",
"issue",
)
result = (text or "").strip().strip('"').strip("'").strip()
changed = True
@@ -476,6 +475,38 @@ class LLMClient:
data = self._parse_json(self._raw_call(system, prompt, temperature=0.9))
return self._normalize_identity(data, archetype)
def generate_handle_candidates(self, archetype: str) -> list[str]:
interests = ", ".join(SEARCH_TERMS.get(archetype, []))
system = (
"You invent online handles for a developer signing up to a programmer "
"community in the style of devRant or Hacker News. The handles read like a "
"real nerd picked them, never like a person's full name. Lean on programming "
"and hacker culture: tech nouns (null, kernel, segfault, daemon), occasional "
"leetspeak (c0d3r, h4x), an adjective plus noun, a creature, or a short word "
f"with a number. Lowercase mostly, {MAX_HANDLE_LEN} characters or fewer, only "
"letters, digits, underscores and hyphens, no spaces and no dots. Return ONLY "
f'a JSON object {{"handles": ["...", "..."]}} with {HANDLE_CANDIDATE_TARGET} '
"distinct handles, no prose, no markdown fences. No em dashes."
)
prompt = (
f"Archetype: {archetype}\n"
f"Their interests: {interests}\n"
"Create the handles JSON:"
)
try:
data = self._parse_json(
self._raw_call(system, prompt, temperature=1.0)
)
except (ValueError, json.JSONDecodeError) as exc:
logger.warning("Handle candidate generation failed: %s", exc)
return []
handles: list[str] = []
for raw in data.get("handles", []):
handle = sanitize_handle(raw)
if handle and handle.lower() not in {h.lower() for h in handles}:
handles.append(handle)
return handles
@staticmethod
def _identity_card(identity: dict) -> str:
lines = [
@@ -586,13 +617,13 @@ class LLMClient:
break
return result
def generate_bug(self, topic: str) -> tuple[str, str]:
def generate_issue(self, topic: str) -> tuple[str, str]:
text = self._call(
"Write a bug report. One line title. Then 2-3 sentences describing what happened and what should have happened. Like a real dev reporting a bug. No em dashes.",
f"Bug: {topic}",
"Write an issue report. One line title. Then 2-3 sentences describing what happened and what should have happened. Like a real dev reporting an issue. No em dashes.",
f"Issue: {topic}",
)
lines = text.strip().split("\n")
title = lines[0].strip().lstrip("#").strip()[:120] or f"Bug report: {topic}"
title = lines[0].strip().lstrip("#").strip()[:120] or f"Issue report: {topic}"
desc = " ".join(
line.lstrip("-* ").strip() for line in lines[1:] if line.strip()
)
+5 -7
View File
@@ -1,9 +1,9 @@
# retoor <retoor@molodetz.nl>
import json
import logging
import time
import urllib.request
from devplacepy import stealth
logger = logging.getLogger(__name__)
@@ -23,11 +23,9 @@ class NewsFetcher:
if fresh:
return self.articles
try:
req = urllib.request.Request(
self.news_api, headers={"User-Agent": "dpbot/1.0"}
)
with urllib.request.urlopen(req, timeout=10) as resp:
articles = json.loads(resp.read()).get("articles", [])
with stealth.stealth_sync_client(timeout=10) as client:
resp = client.get(self.news_api)
articles = resp.json().get("articles", [])
if articles:
self.articles = articles
self.fetched_at = time.monotonic()
+2 -1
View File
@@ -15,6 +15,7 @@ class BotState:
password: str = ""
uid: str = ""
persona: str = ""
handle_candidates: list[str] = field(default_factory=list)
profile_filled: bool = False
user_agent: str = ""
viewport: dict = field(default_factory=dict)
@@ -27,7 +28,7 @@ class BotState:
comments_posted: int = 0
quality_rejections: int = 0
votes_cast: int = 0
bugs_filed: int = 0
issues_filed: int = 0
projects_created: int = 0
gists_created: int = 0
profiles_viewed: int = 0