Skip to content

Commit c029336

Browse files
committed
fix uses a high-entropy seed
1 parent d2f5bd6 commit c029336

1 file changed

Lines changed: 46 additions & 31 deletions

File tree

cosmicexcuse/generator.py

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
"""
44

55
import hashlib
6+
import itertools
7+
import os
68
import random
9+
import threading
710
import time
811
from dataclasses import dataclass
912
from pathlib import Path
@@ -44,6 +47,10 @@ class Excuse:
4447
metadata: Dict[str, Any]
4548

4649

50+
# Monotonic counter to guarantee changing seeds across rapid calls
51+
_SEED_COUNTER = itertools.count()
52+
53+
4754
class ExcuseGenerator:
4855
"""
4956
Base excuse generator class.
@@ -82,13 +89,13 @@ def __init__(self, language: str = "en", data_path: Optional[Path] = None):
8289
def _build_markov_chain(self):
8390
"""Build Markov chain from technical terms."""
8491
# Get technical terms from data
85-
technical_terms = []
92+
technical_terms: List[str] = []
8693
for category in ["quantum", "technical", "ai"]:
8794
if category in self.data:
8895
technical_terms.extend(self.data[category])
8996

9097
# Extract individual words and build corpus
91-
corpus = []
98+
corpus: List[str] = []
9299
for term in technical_terms:
93100
corpus.extend(term.split())
94101

@@ -113,22 +120,22 @@ def generate(
113120
"""
114121
severity = self.analyzer.analyze(error_message)
115122

116-
# Generate quantum seed for reproducible randomness
123+
# Create a local RNG seeded with high-entropy seed
117124
quantum_seed = self._generate_quantum_seed(error_message)
118-
random.seed(quantum_seed)
125+
rng = random.Random(quantum_seed)
119126

120127
# Select categories
121128
if category and category in self.data:
122129
primary_category = category
123130
else:
124-
primary_category = random.choice(list(self.data.keys()))
131+
primary_category = rng.choice(list(self.data.keys()))
125132

126133
# Ensure we don't pick recommendations or connectors as primary
127134
while primary_category in ["recommendations", "connectors", "intensifiers"]:
128-
primary_category = random.choice(list(self.data.keys()))
135+
primary_category = rng.choice(list(self.data.keys()))
129136

130137
# Get excuse components
131-
primary_excuse = random.choice(self.data[primary_category])
138+
primary_excuse = rng.choice(self.data[primary_category])
132139

133140
# Get secondary category
134141
available_categories = [
@@ -137,16 +144,16 @@ def generate(
137144
if k
138145
not in [primary_category, "recommendations", "connectors", "intensifiers"]
139146
]
140-
secondary_category = random.choice(available_categories)
141-
secondary_excuse = random.choice(self.data[secondary_category])
147+
secondary_category = rng.choice(available_categories)
148+
secondary_excuse = rng.choice(self.data[secondary_category])
142149

143150
# Get intensifier based on severity
144-
intensifier = self._get_intensifier(severity)
151+
intensifier = self._get_intensifier(severity, rng)
145152

146153
# Get connector
147-
connector = random.choice(self.data.get("connectors", ["which caused"]))
154+
connector = rng.choice(self.data.get("connectors", ["which caused"]))
148155

149-
# Generate Markov nonsense
156+
# Generate Markov nonsense (uses module RNG internally; that's fine)
150157
markov_phrase = self.markov.generate(length=5)
151158

152159
# Construct the excuse
@@ -160,7 +167,7 @@ def generate(
160167
)
161168

162169
# Get recommendation
163-
recommendation = random.choice(
170+
recommendation = rng.choice(
164171
self.data.get("recommendations", ["Try turning it off and on again"])
165172
)
166173

@@ -174,7 +181,7 @@ def generate(
174181
severity=severity,
175182
category=primary_category,
176183
quality_score=quality_score,
177-
quantum_probability=random.random(),
184+
quantum_probability=rng.random(),
178185
language=self.language,
179186
timestamp=time.time(),
180187
metadata={
@@ -188,12 +195,20 @@ def generate(
188195
return excuse
189196

190197
def _generate_quantum_seed(self, error_message: str) -> int:
191-
"""Generate a 'quantum' seed from the error message."""
192-
hash_input = f"{error_message}{time.time()}"
193-
hash_hex = hashlib.md5(hash_input.encode()).hexdigest()[:8]
194-
return int(hash_hex, 16)
195-
196-
def _get_intensifier(self, severity: str) -> str:
198+
"""
199+
Generate a high-entropy 64-bit seed that changes on every call,
200+
even when invoked multiple times within the same OS clock tick.
201+
"""
202+
t1 = time.time_ns()
203+
t2 = time.perf_counter_ns() # monotonic, high-res
204+
pid = os.getpid()
205+
tid = threading.get_ident()
206+
ctr = next(_SEED_COUNTER)
207+
h = hashlib.blake2b(digest_size=8)
208+
h.update(f"{error_message}|{t1}|{t2}|{pid}|{tid}|{ctr}".encode("utf-8"))
209+
return int.from_bytes(h.digest(), "big")
210+
211+
def _get_intensifier(self, severity: str, rng: random.Random) -> str:
197212
"""Get an intensifier based on severity."""
198213
intensifiers = self.data.get("intensifiers", {})
199214

@@ -202,7 +217,7 @@ def _get_intensifier(self, severity: str) -> str:
202217
else:
203218
severity_intensifiers = ["definitely"]
204219

205-
return random.choice(severity_intensifiers)
220+
return rng.choice(severity_intensifiers)
206221

207222
def _calculate_quality_score(self, excuse_text: str, seed: int) -> int:
208223
"""Calculate a 'quality score' for the excuse."""
@@ -219,15 +234,10 @@ def _calculate_quality_score(self, excuse_text: str, seed: int) -> int:
219234

220235
def generate_batch(self, count: int = 5) -> List[Excuse]:
221236
"""
222-
Generate multiple excuses.
223-
224-
Args:
225-
count: Number of excuses to generate
226-
227-
Returns:
228-
List of Excuse objects
237+
Generate multiple excuses, ensuring textual uniqueness.
229238
"""
230-
excuses = []
239+
excuses: List[Excuse] = []
240+
seen: set[str] = set()
231241

232242
# Sample error messages for variety
233243
sample_errors = [
@@ -241,10 +251,15 @@ def generate_batch(self, count: int = 5) -> List[Excuse]:
241251
"MemoryError: Out of memory",
242252
]
243253

244-
for _ in range(count):
254+
attempts = 0
255+
max_attempts = count * 10
256+
while len(excuses) < count and attempts < max_attempts:
245257
error = random.choice(sample_errors)
246258
excuse = self.generate(error)
247-
excuses.append(excuse)
259+
if excuse.text not in seen:
260+
seen.add(excuse.text)
261+
excuses.append(excuse)
262+
attempts += 1
248263

249264
return excuses
250265

0 commit comments

Comments
 (0)