mervenoyan commited on
Commit
fabd7ab
·
1 Parent(s): b6b084e

Add source session id to each sin and force smart-quote wrapping

Browse files

Schema 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.

Files changed (2) hide show
  1. analyze.py +23 -15
  2. 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 the user from a top_quotes entry, wrapped in smart quotes no paraphrasing, no citation, no commentary>"},
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 (a verbatim user quote, smart-quoted)
 
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, wrapped in smart quotes ( " " ). No paraphrasing, no rewording, no citation text, 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.
169
- 3. 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.
170
- 4. No PII. No emails, no real names, no private repos. Public handles and public dataset names are fine.
171
- 5. 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.
 
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, then pick a verbatim top_quote that proves the sin and place it (smart-quoted) as the meta.
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's `meta` MUST be a verbatim "
195
- "top_quote from the digests, wrapped in smart quotes — no paraphrasing, "
196
- "no citation, no analysis. The `title` is the roast; the `meta` is the "
197
- "user's own words. Tagline ≤170 chars; forecast.body ≤340 chars."
 
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
- {"n": str(s.get("n") or f"{i+1:02d}"), "title": str(s.get("title") or "—"), "meta": str(s.get("meta") or "—")}
 
 
 
 
 
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">{e(str(sin.get("meta") or ""))}</span>'
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;