Commit ·
fabd7ab
1
Parent(s): b6b084e
Add source session id to each sin and force smart-quote wrapping
Browse filesSchema gets a new 'source' field per sin = the session_id of the
digest the quote came from. render.py strips any quote marks the model
may emit and always wraps the meta in smart quotes itself, so the
quotation marks are guaranteed. The source renders inline after the
quote in a dimmer, non-italic mono style.
- analyze.py +23 -15
- render.py +17 -1
analyze.py
CHANGED
|
@@ -141,9 +141,9 @@ Return EXACTLY one JSON object, no prose, no markdown:
|
|
| 141 |
"archetype": ["The <adjective>", "<Noun>"],
|
| 142 |
"tagline": "<130-170 chars, 2-3 italic lines, sentences only, end on a punchline>",
|
| 143 |
"sins": [
|
| 144 |
-
{"n":"01","title":"<50-90 chars: one concrete user behaviour, sentence case, no quotes>","meta":"<30-110 chars: a VERBATIM quote from
|
| 145 |
-
{"n":"02","title":"...","meta":"..."},
|
| 146 |
-
{"n":"03","title":"...","meta":"..."}
|
| 147 |
],
|
| 148 |
"forecast": {"headline":"The week ahead","body":"<270-340 chars, horoscope-style, end with 'Lucky <x>: <y>. Avoid: <z>.'>"}
|
| 149 |
}
|
|
@@ -153,7 +153,8 @@ Field budgets (hard limits — overflow breaks the layout):
|
|
| 153 |
- archetype[1]: 6-14 chars (line 2, title-cased punch noun)
|
| 154 |
- tagline: 130-170 chars
|
| 155 |
- sins[].title: 50-90 chars
|
| 156 |
-
- sins[].meta: 30-110 chars (
|
|
|
|
| 157 |
- forecast.body: 270-340 chars, ends with "Lucky <x>: <y>. Avoid: <z>."
|
| 158 |
|
| 159 |
The sins array MUST contain exactly 3 objects. Do not emit fewer.
|
|
@@ -165,15 +166,16 @@ Voice:
|
|
| 165 |
|
| 166 |
Hard rules:
|
| 167 |
1. Roast the USER, not the agent. The user cannot run code; only the agent can. Wrong: "Parsed JSON with a regex twice." (that's the agent). Right: "Asked the agent to parse JSON with a regex twice." / "Demanded a regex over a JSON parser, against advice."
|
| 168 |
-
2. EVERY sins[].meta MUST be a verbatim top_quote from one of the digests
|
| 169 |
-
3.
|
| 170 |
-
4.
|
| 171 |
-
5. No
|
|
|
|
| 172 |
|
| 173 |
Procedure:
|
| 174 |
1. Skim the digests for recurring patterns (repeated questions, premature optimisation, doc avoidance, tone, tool misuse, mood arc).
|
| 175 |
2. Pick ONE crisp archetype. Examples: The Premature Optimizer · The Vibes Driver · The Doc Avoider · The Refactor Romantic · The Confidence Auditor · The Apology Engineer · The TODO Composer. Invent freely.
|
| 176 |
-
3. Pick three sins the digests support. For each: write a roast sentence as the title,
|
| 177 |
4. Tagline: 2-3 short sentences piling on the archetype with concrete examples. End on a punchline.
|
| 178 |
5. Horoscope: one absurd technical prediction grounded in a real user pattern. Close with "Lucky <something>: <x>. Avoid: <y>."
|
| 179 |
6. Validate lengths against budgets. Trim or pad before emitting.
|
|
@@ -191,10 +193,11 @@ def bulletin(
|
|
| 191 |
f"user: {user}\n"
|
| 192 |
f"dataset: {dataset_id}\n\n"
|
| 193 |
f"digests (JSON list):\n{json.dumps(digests, ensure_ascii=False, indent=2)}\n\n"
|
| 194 |
-
"Reminder: emit EXACTLY 3 sins. Each sin
|
| 195 |
-
"
|
| 196 |
-
"
|
| 197 |
-
"
|
|
|
|
| 198 |
)
|
| 199 |
resp = client.chat_completion(
|
| 200 |
messages=[
|
|
@@ -224,14 +227,19 @@ def build_report(
|
|
| 224 |
if not isinstance(archetype, list) or len(archetype) < 2:
|
| 225 |
archetype = ["The", "Unreadable"]
|
| 226 |
sins = data.get("sins") or []
|
| 227 |
-
sins = sins[:3] + [{"n": f"{i+1:02d}", "title": "—", "meta": "—"} for i in range(len(sins), 3)]
|
| 228 |
forecast = data.get("forecast") or {"headline": "The week ahead", "body": "The cards are quiet today."}
|
| 229 |
return {
|
| 230 |
"user": str(data.get("user") or user),
|
| 231 |
"archetype": [str(archetype[0]), str(archetype[1])],
|
| 232 |
"tagline": str(data.get("tagline") or ""),
|
| 233 |
"sins": [
|
| 234 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
for i, s in enumerate(sins[:3])
|
| 236 |
],
|
| 237 |
"forecast": {
|
|
|
|
| 141 |
"archetype": ["The <adjective>", "<Noun>"],
|
| 142 |
"tagline": "<130-170 chars, 2-3 italic lines, sentences only, end on a punchline>",
|
| 143 |
"sins": [
|
| 144 |
+
{"n":"01","title":"<50-90 chars: one concrete user behaviour, sentence case, no quotes>","meta":"<30-110 chars: a VERBATIM quote from a top_quotes entry — raw text only, NO surrounding quote marks (render adds them)>","source":"<the exact session_id of the digest the quote was taken from>"},
|
| 145 |
+
{"n":"02","title":"...","meta":"...","source":"..."},
|
| 146 |
+
{"n":"03","title":"...","meta":"...","source":"..."}
|
| 147 |
],
|
| 148 |
"forecast": {"headline":"The week ahead","body":"<270-340 chars, horoscope-style, end with 'Lucky <x>: <y>. Avoid: <z>.'>"}
|
| 149 |
}
|
|
|
|
| 153 |
- archetype[1]: 6-14 chars (line 2, title-cased punch noun)
|
| 154 |
- tagline: 130-170 chars
|
| 155 |
- sins[].title: 50-90 chars
|
| 156 |
+
- sins[].meta: 30-110 chars (raw verbatim user quote, no surrounding quote marks)
|
| 157 |
+
- sins[].source: the session_id from the digest the quote came from
|
| 158 |
- forecast.body: 270-340 chars, ends with "Lucky <x>: <y>. Avoid: <z>."
|
| 159 |
|
| 160 |
The sins array MUST contain exactly 3 objects. Do not emit fewer.
|
|
|
|
| 166 |
|
| 167 |
Hard rules:
|
| 168 |
1. Roast the USER, not the agent. The user cannot run code; only the agent can. Wrong: "Parsed JSON with a regex twice." (that's the agent). Right: "Asked the agent to parse JSON with a regex twice." / "Demanded a regex over a JSON parser, against advice."
|
| 169 |
+
2. EVERY sins[].meta MUST be a verbatim top_quote from one of the digests. Emit the raw text only — NO surrounding quote marks (the renderer wraps it). No paraphrasing, no rewording, no analysis. Just the user's own words. If no top_quote fits a sin you've drafted, pick a different sin that does have a fitting quote.
|
| 170 |
+
3. EVERY sins[].source MUST be the exact `session_id` value of the digest the quote came from. Copy it verbatim — do not shorten, rename, or invent.
|
| 171 |
+
4. The title is the roast sentence (no quotes inside it); the meta below it is the receipt — the user's own words that prove the sin. Title and meta must be different content, not paraphrases of each other.
|
| 172 |
+
5. No PII. No emails, no real names, no private repos. Public handles and public dataset names are fine.
|
| 173 |
+
6. No identity punching. Roast process and habits — not who the user is. Off-limits: appearance, nationality, gender, politics, illness. Fair game: ignoring docs, refactor addiction, regex misuse, vibes-driven coding, asking the same thing six ways, premature optimisation, late-night commits.
|
| 174 |
|
| 175 |
Procedure:
|
| 176 |
1. Skim the digests for recurring patterns (repeated questions, premature optimisation, doc avoidance, tone, tool misuse, mood arc).
|
| 177 |
2. Pick ONE crisp archetype. Examples: The Premature Optimizer · The Vibes Driver · The Doc Avoider · The Refactor Romantic · The Confidence Auditor · The Apology Engineer · The TODO Composer. Invent freely.
|
| 178 |
+
3. Pick three sins the digests support. For each: write a roast sentence as the title, pick a verbatim top_quote that proves the sin and place it (raw, no quote marks) as the meta, and set source to that digest's session_id.
|
| 179 |
4. Tagline: 2-3 short sentences piling on the archetype with concrete examples. End on a punchline.
|
| 180 |
5. Horoscope: one absurd technical prediction grounded in a real user pattern. Close with "Lucky <something>: <x>. Avoid: <y>."
|
| 181 |
6. Validate lengths against budgets. Trim or pad before emitting.
|
|
|
|
| 193 |
f"user: {user}\n"
|
| 194 |
f"dataset: {dataset_id}\n\n"
|
| 195 |
f"digests (JSON list):\n{json.dumps(digests, ensure_ascii=False, indent=2)}\n\n"
|
| 196 |
+
"Reminder: emit EXACTLY 3 sins. Each sin needs `title` (the roast), "
|
| 197 |
+
"`meta` (a VERBATIM top_quote, raw text only — no surrounding quote "
|
| 198 |
+
"marks; the renderer wraps them), and `source` (the session_id of the "
|
| 199 |
+
"digest the quote was taken from, copied verbatim). "
|
| 200 |
+
"Tagline ≤170 chars; forecast.body ≤340 chars."
|
| 201 |
)
|
| 202 |
resp = client.chat_completion(
|
| 203 |
messages=[
|
|
|
|
| 227 |
if not isinstance(archetype, list) or len(archetype) < 2:
|
| 228 |
archetype = ["The", "Unreadable"]
|
| 229 |
sins = data.get("sins") or []
|
| 230 |
+
sins = sins[:3] + [{"n": f"{i+1:02d}", "title": "—", "meta": "—", "source": ""} for i in range(len(sins), 3)]
|
| 231 |
forecast = data.get("forecast") or {"headline": "The week ahead", "body": "The cards are quiet today."}
|
| 232 |
return {
|
| 233 |
"user": str(data.get("user") or user),
|
| 234 |
"archetype": [str(archetype[0]), str(archetype[1])],
|
| 235 |
"tagline": str(data.get("tagline") or ""),
|
| 236 |
"sins": [
|
| 237 |
+
{
|
| 238 |
+
"n": str(s.get("n") or f"{i+1:02d}"),
|
| 239 |
+
"title": str(s.get("title") or "—"),
|
| 240 |
+
"meta": str(s.get("meta") or "—"),
|
| 241 |
+
"source": str(s.get("source") or ""),
|
| 242 |
+
}
|
| 243 |
for i, s in enumerate(sins[:3])
|
| 244 |
],
|
| 245 |
"forecast": {
|
render.py
CHANGED
|
@@ -99,12 +99,20 @@ def _inner_html(data: dict) -> str:
|
|
| 99 |
'<span class="tpb-rule"></span>'
|
| 100 |
"</div>"
|
| 101 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
sin_blocks.append(
|
| 103 |
'<div class="tpb-sin-row">'
|
| 104 |
f'<div class="tpb-sin-n">{e(_ROMAN[i])}.</div>'
|
| 105 |
'<div class="tpb-sin-body">'
|
| 106 |
f'<p class="tpb-sin-title">{e(str(sin.get("title") or ""))}</p>'
|
| 107 |
-
f'<span class="tpb-sin-meta">{
|
| 108 |
"</div></div>"
|
| 109 |
)
|
| 110 |
sins_html = "".join(sin_blocks)
|
|
@@ -345,6 +353,14 @@ def _inner_html(data: dict) -> str:
|
|
| 345 |
color: {_SINS_INK};
|
| 346 |
opacity: 0.85;
|
| 347 |
letter-spacing: 0.04em;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
}}
|
| 349 |
.tpb-sin-divider {{
|
| 350 |
display: flex; align-items: center; gap: 12px;
|
|
|
|
| 99 |
'<span class="tpb-rule"></span>'
|
| 100 |
"</div>"
|
| 101 |
)
|
| 102 |
+
raw_meta = str(sin.get("meta") or "").strip()
|
| 103 |
+
# Strip any quote marks the model may have wrapped around the quote
|
| 104 |
+
# so the rendered card always uses the same smart-quote pair.
|
| 105 |
+
raw_meta = raw_meta.strip("\"'“”‘’")
|
| 106 |
+
meta_html = f'“{e(raw_meta)}”' if raw_meta and raw_meta != "—" else e(raw_meta)
|
| 107 |
+
source = str(sin.get("source") or "").strip()
|
| 108 |
+
if source:
|
| 109 |
+
meta_html += f' <span class="tpb-sin-source">· {e(source)}</span>'
|
| 110 |
sin_blocks.append(
|
| 111 |
'<div class="tpb-sin-row">'
|
| 112 |
f'<div class="tpb-sin-n">{e(_ROMAN[i])}.</div>'
|
| 113 |
'<div class="tpb-sin-body">'
|
| 114 |
f'<p class="tpb-sin-title">{e(str(sin.get("title") or ""))}</p>'
|
| 115 |
+
f'<span class="tpb-sin-meta">{meta_html}</span>'
|
| 116 |
"</div></div>"
|
| 117 |
)
|
| 118 |
sins_html = "".join(sin_blocks)
|
|
|
|
| 353 |
color: {_SINS_INK};
|
| 354 |
opacity: 0.85;
|
| 355 |
letter-spacing: 0.04em;
|
| 356 |
+
font-style: italic;
|
| 357 |
+
}}
|
| 358 |
+
.tpb-sin-source {{
|
| 359 |
+
font-style: normal;
|
| 360 |
+
font-weight: 400;
|
| 361 |
+
opacity: 0.6;
|
| 362 |
+
letter-spacing: 0.06em;
|
| 363 |
+
margin-left: 2px;
|
| 364 |
}}
|
| 365 |
.tpb-sin-divider {{
|
| 366 |
display: flex; align-items: center; gap: 12px;
|