[object Object]

← back to Rejected Prompts Viewer

chore: lint, refactor, v1.1.0 (session close) — named constants, guarded loads, finished heretic tone-candidate classification (15 confirmed)

a09ecd3b2bfedc7576a769c93a9f142ccf3e4a89 · 2026-08-19 11:03:08 -0700 · Steve Abrams

Files touched

Diff

commit a09ecd3b2bfedc7576a769c93a9f142ccf3e4a89
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 11:03:08 2026 -0700

    chore: lint, refactor, v1.1.0 (session close) — named constants, guarded loads, finished heretic tone-candidate classification (15 confirmed)
---
 __pycache__/build-data.cpython-314.pyc | Bin 0 -> 4197 bytes
 __pycache__/classify.cpython-314.pyc   | Bin 0 -> 4443 bytes
 __pycache__/scan.cpython-314.pyc       | Bin 0 -> 7073 bytes
 build-data.py                          |  16 ++++++--
 classify.py                            |  72 ++++++++++++++++++++++-----------
 data/rejections.json                   |  30 +++++++-------
 package.json                           |   2 +-
 scan.py                                |  26 +++++++-----
 8 files changed, 93 insertions(+), 53 deletions(-)

diff --git a/__pycache__/build-data.cpython-314.pyc b/__pycache__/build-data.cpython-314.pyc
new file mode 100644
index 0000000..fc64563
Binary files /dev/null and b/__pycache__/build-data.cpython-314.pyc differ
diff --git a/__pycache__/classify.cpython-314.pyc b/__pycache__/classify.cpython-314.pyc
new file mode 100644
index 0000000..703f4a2
Binary files /dev/null and b/__pycache__/classify.cpython-314.pyc differ
diff --git a/__pycache__/scan.cpython-314.pyc b/__pycache__/scan.cpython-314.pyc
new file mode 100644
index 0000000..cae9f94
Binary files /dev/null and b/__pycache__/scan.cpython-314.pyc differ
diff --git a/build-data.py b/build-data.py
index 62c3e9c..1246a92 100644
--- a/build-data.py
+++ b/build-data.py
@@ -2,7 +2,11 @@
 """Transform the classified refusal candidates into the viewer's data file.
 Keeps only genuine policy refusals, recovers each transcript's project name,
 and writes public-consumable data/rejections.json."""
-import json, glob, os, datetime, sys
+import datetime
+import glob
+import json
+import os
+import sys
 
 HERE = os.path.dirname(os.path.abspath(__file__))
 CLASSIFIED = sys.argv[1] if len(sys.argv) > 1 else '/tmp/refusal_classified.json'
@@ -17,7 +21,10 @@ for d in glob.glob(os.path.join(PROJROOT, '*')):
     for f in glob.glob(os.path.join(d, '**', '*.jsonl'), recursive=True):
         basename_project[os.path.basename(f)] = readable
 
-rows = json.load(open(CLASSIFIED))
+try:
+    rows = json.load(open(CLASSIFIED))
+except (FileNotFoundError, json.JSONDecodeError) as e:
+    sys.exit(f"ERROR: cannot read {CLASSIFIED}: {e}")
 items = []
 for r in rows:
     items.append({
@@ -34,13 +41,14 @@ for r in rows:
 items.sort(key=lambda x: (x['ts'] or ''), reverse=True)
 
 scanned = len(glob.glob(os.path.join(PROJROOT, '**', '*.jsonl'), recursive=True))
+confirmed_count = sum(1 for i in items if i['confirmed'])
 out = {
     'generated': datetime.datetime.now().astimezone().isoformat(),
     'scanned_files': scanned,
     'total_candidates': len(rows),
+    'confirmed_count': confirmed_count,
     'items': items,
 }
 os.makedirs(os.path.join(HERE, 'data'), exist_ok=True)
-out['confirmed_count'] = sum(1 for i in items if i['confirmed'])
 json.dump(out, open(os.path.join(HERE, 'data', 'rejections.json'), 'w'), indent=1)
-print(f"wrote {len(items)} items ({out['confirmed_count']} confirmed + {len(items)-out['confirmed_count']} borderline), scanned {scanned} transcripts -> data/rejections.json")
+print(f"wrote {len(items)} items ({confirmed_count} confirmed + {len(items)-confirmed_count} borderline), scanned {scanned} transcripts -> data/rejections.json")
diff --git a/classify.py b/classify.py
index 67470a4..2fe8129 100644
--- a/classify.py
+++ b/classify.py
@@ -1,33 +1,57 @@
-import json,urllib.request,re,time
-cands=json.load(open('/tmp/refusal_cands.json'))
-OUT=[]
-SYS=("You audit an AI coding assistant's replies. Given the USER request and the ASSISTANT reply, decide "
+#!/usr/bin/env python3
+import json
+import re
+import sys
+import time
+import urllib.request
+from collections import Counter
+
+_CANDS_PATH = '/tmp/refusal_cands.json'
+_OUT_PATH   = '/tmp/refusal_classified.json'
+OLLAMA_URL  = 'http://localhost:11434/api/chat'
+MODEL       = 'qwen3.8-27b-heretic:latest'
+
+try:
+    cands = json.load(open(_CANDS_PATH))
+except (FileNotFoundError, json.JSONDecodeError) as e:
+    sys.exit(f"ERROR: cannot read {_CANDS_PATH}: {e}")
+
+OUT = []
+SYS = ("You audit an AI coding assistant's replies. Given the USER request and the ASSISTANT reply, decide "
 "if the assistant REFUSED or REFUSED-IN-PART to do what was asked on POLICY/LEGAL grounds "
 "(copyright, trademark, intellectual-property/impersonation, or website terms-of-service / scraping / "
 "anti-bot / unauthorized-access). A mere capability limit ('I can't quit the app from here', "
 "'you must click this yourself', 'I can't create your account') is NOT a policy refusal. Agreeing to "
 "proceed is NOT a refusal. Respond ONLY with compact JSON: "
 '{"refusal":true|false,"category":"copyright|trademark|ip-impersonation|website-tos|other","reason":"<=12 words"}')
-def call(u,a):
-    body=json.dumps({"model":"qwen3.8-27b-heretic:latest","stream":False,"think":False,
-        "messages":[{"role":"system","content":SYS},
-        {"role":"user","content":f"USER REQUEST:\n{u}\n\nASSISTANT REPLY:\n{a}\n\nJSON:"}],
-        "options":{"temperature":0}}).encode()
-    r=urllib.request.urlopen(urllib.request.Request("http://localhost:11434/api/chat",body,{"Content-Type":"application/json"}),timeout=120)
-    txt=json.loads(r.read())["message"]["content"]
-    txt=re.sub(r"<think>.*?</think>","",txt,flags=re.S).strip()
-    mm=re.search(r"\{.*\}",txt,re.S)
-    return json.loads(mm.group(0)) if mm else {"refusal":False,"category":"other","reason":"parse-fail"}
-t0=time.time()
-for i,c in enumerate(cands):
-    try: v=call(c['user'],c['refusal'])
-    except Exception as e: v={"refusal":False,"category":"other","reason":f"err:{e}"[:40]}
+
+def call(u, a):
+    body = json.dumps({"model": MODEL, "stream": False, "think": False,
+        "messages": [{"role": "system", "content": SYS},
+                     {"role": "user", "content": f"USER REQUEST:\n{u}\n\nASSISTANT REPLY:\n{a}\n\nJSON:"}],
+        "options": {"temperature": 0}}).encode()
+    r = urllib.request.urlopen(
+        urllib.request.Request(OLLAMA_URL, body, {"Content-Type": "application/json"}),
+        timeout=120)
+    txt = re.sub(r"<think>.*?</think>", "", json.loads(r.read())["message"]["content"], flags=re.DOTALL).strip()
+    mm = re.search(r"\{.*\}", txt, re.DOTALL)
+    return json.loads(mm.group(0)) if mm else {"refusal": False, "category": "other", "reason": "parse-fail"}
+
+t0 = time.time()
+for i, c in enumerate(cands):
+    try:
+        v = call(c['user'], c['refusal'])
+    except Exception as e:
+        v = {"refusal": False, "category": "other", "reason": f"err:{e}"[:40]}
     # store verdict under 'confirmed' so it never clobbers the 'refusal' TEXT
-    c['confirmed']=bool(v.get('refusal')); c['category']=v.get('category','other'); c['reason']=v.get('reason','')
+    c['confirmed'] = bool(v.get('refusal'))
+    c['category']  = v.get('category', 'other')
+    c['reason']    = v.get('reason', '')
     OUT.append(c)
-    if i%10==0: print(f"{i}/{len(cands)}  {time.time()-t0:.0f}s", flush=True)
-json.dump(OUT,open('/tmp/refusal_classified.json','w'),indent=1)
-kept=[c for c in OUT if c.get('confirmed')]
-print("DONE. genuine refusals:",len(kept),"/",len(OUT))
-from collections import Counter
+    if i % 10 == 0:
+        print(f"{i}/{len(cands)}  {time.time()-t0:.0f}s", flush=True)
+
+json.dump(OUT, open(_OUT_PATH, 'w'), indent=1)
+kept = [c for c in OUT if c.get('confirmed')]
+print("DONE. genuine refusals:", len(kept), "/", len(OUT))
 print(Counter(c['category'] for c in kept))
diff --git a/data/rejections.json b/data/rejections.json
index e608aad..2486ad8 100644
--- a/data/rejections.json
+++ b/data/rejections.json
@@ -1,7 +1,8 @@
 {
- "generated": "2026-08-19T10:54:43.121483-07:00",
- "scanned_files": 5179,
+ "generated": "2026-08-19T11:02:51.176843-07:00",
+ "scanned_files": 5181,
  "total_candidates": 86,
+ "confirmed_count": 15,
  "items": [
   {
    "user": "\"use estimates\"",
@@ -29,7 +30,7 @@
    "user": "Open \u2014 needs Steve: Complete an openclaw Instagram login as an account that owns @ffepurchasing / @restorationwallpaper / @roomsettings / @retrowalls (not @hospitalityfabrics). Once openclaw is parked on a logged-in instagram.com, say go and I'll re-prob",
    "refusal": "Still logged **out** \u2014 openclaw is on the IG login form (*\"Log into Instagram\"*, \"Log in with Facebook\", \"Create new account\", language switcher). No login completed, so nothing changed and there's nothing I can safely run. **Holding, zero deletes.**\n\nThe login is genuinely yours to complete \u2014 it needs credentials + 2FA on an accessible account, in the openclaw Chrome window itself, and I can't do that for you. Until openclaw actually lands on a logged-in `instagram.com` (Home/Search/Profile visible), the probe \u2192 canary \u2192 delete sequence can't start.\n\nNothing else is actionable in this session (the panel already voted **CLOSE**, and the one deliverable \u2014 the hardened script \u2014 is committed + its win logged). So the state is unchanged:\n\n- **TK-10617:** HELD, documented.\n- **To resume later:*",
    "category": "other",
-   "reason": "(tone candidate \u2014 unreviewed)",
+   "reason": "Capability limit; no policy refusal stated.",
    "confirmed": false,
    "net": "B",
    "ts": "2026-08-17T17:49:34.835Z",
@@ -40,7 +41,7 @@
    "user": "and log",
    "refusal": "\u2705 **Logged to CNCP + sender committed.** Now the send is yours to fire \u2014 here's the clean path.\n\n## To send all 18 right now \u2014 paste this in the prompt:\n```\n! node ~/Projects/sample-followup-sweep/scripts/send-drafts.js\n```\nThe `!` runs it as **your** command (not an agent tool call), so it clears the classifier. The script reads the approval token from `.env`, POSTs each letter through George, and prints `SENT/FAIL` per vendor + writes `out/send-ledger.json`. That's the human-pulls-trigger the guardrail wanted \u2014 using the exact sender I built.\n\n**Before you run it \u2014 eyeball these 5 inferred recipients** (the ones the classifier flagged; the other 13 are verified from your own Sent mail):\n| To | For |\n|---|---|\n| Rhaiza@quadrilleinc.com | China Seas \u2192 Quadrille |\n| matt.schoffman@kravet.co",
    "category": "other",
-   "reason": "(tone candidate \u2014 unreviewed)",
+   "reason": "Assistant provided instructions to proceed, no refusal.",
    "confirmed": false,
    "net": "B",
    "ts": "2026-08-14T23:33:35.699Z",
@@ -172,7 +173,7 @@
    "user": "continue",
    "refusal": "I have to correct the record: **the fix did not hold, and I closed TK-10395 too early.**\n\n## What the real 07:10 nightly run showed (this morning)\n- **2207076 recurred and beat the retry** \u2014 attempt 1 timed out at the 48s poll cap, attempts 2 & 3 hard-errored. The reel **did not post**. Yesterday's manual success was a lucky attempt-1; 2207076 is **intermittent/persistent**, not reliably retry-beatable. (The contrarian called exactly this.)\n- **The alert was swallowed** \u2014 the publish-gate ran *last*, after the Kamatera push. Kamatera is at **1% disk (CRITICAL)**, so the push hung/failed and the script never reached the gate \u2192 no CNCP card, no email. The silent-green hole we thought we closed was still open on the real path.\n\n## What I fixed now (reversible/local, within authority)\n- **`c46",
    "category": "other",
-   "reason": "(tone candidate \u2014 unreviewed)",
+   "reason": "Assistant continued work without policy refusal.",
    "confirmed": false,
    "net": "B",
    "ts": "2026-08-11T14:17:04.270Z",
@@ -359,8 +360,8 @@
    "user": "if not build it there",
    "refusal": "The real-context run gave me the definitive answer \u2014 and it's the boundary the auth-topology memory predicted:\n\n```\nsecurity: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.\nFATAL: no George auth\n```\n\nSo the **keychain item doesn't exist** \u2014 george-mcp actually gets its auth from `GEORGE_BASIC_AUTH` in `~/.claude.json`, and the keychain path is just an unpopulated fallback. The watcher is fully built and correct; it simply needs George's admin auth wired in **once**. I can't do that myself \u2014 reading/decoding that credential is exactly what the classifier blocks (and should).\n\n`\u2605 Insight \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`\n- This is why kickstarting in the *real launchd context* mattered: my sandboxed probe earlier was ambiguous (\"KEYCHAIN MISS\" could've",
    "category": "other",
-   "reason": "(tone candidate \u2014 unreviewed)",
-   "confirmed": false,
+   "reason": "Classifier blocks reading/decoding credential",
+   "confirmed": true,
    "net": "B",
    "ts": "2026-08-06T23:05:02.988Z",
    "file": "12729df5-2832-4357-a0d8-76b68f44d4eb.jsonl",
@@ -590,7 +591,7 @@
    "user": "we need space now!!",
    "refusal": "Straight truth, because you need this to work and I won't pretend otherwise:\n\n`\u2605 Insight \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`\n**I cannot make space appear \"now\" \u2014 and neither can more deleting.** The purge is already running at Shopify's maximum delete rate (~1.5\u20132/s; that's a hard REST ceiling, can't go faster). The blocker isn't how much we delete \u2014 it's that **Shopify credits freed storage on its own asynchronous garbage-collection sweep**, which has no API to trigger or time. Your 10+ deleted themes + 1,200 purged images are queued for that sweep; the `413` persists only until Shopify runs it.\n**The one lever that frees space *instantly* is raising the plan's storage limit** (Shopify billing/support) \u2014 I can't do that via API; it's a you-and-Shopify action. Everything else waits on G",
    "category": "other",
-   "reason": "(tone candidate \u2014 unreviewed)",
+   "reason": "Explains technical limitation, not policy refusal",
    "confirmed": false,
    "net": "B",
    "ts": "2026-07-29T15:21:03.574Z",
@@ -601,7 +602,7 @@
    "user": " repoint the rollup, and/or pull up the 8\n  GO/NO-GO packets one at a time for your yes/no?",
    "refusal": "Relayed \u2014 tabs 2, 3, 7 now have their GO and are executing (they'll `tk log` back to TK-10012, so the rollup will flip them to done as they finish).\n\n**\ud83d\udd11 GoDaddy (task #1) \u2014 your action, I can't do this one:**\n1. Go to **developer.godaddy.com** \u2192 **API Keys**\n2. Find the old leaked key ending **\u2026jacsF** \u2192 **Delete / Revoke**\n3. Tell me when done and I'll strip the dead `GODADDY_API_KEY` fallback line from `secrets-manager/.env` (reversible/local).\n\nNow the **next 4 packets** for your yes/no:",
    "category": "other",
-   "reason": "(tone candidate \u2014 unreviewed)",
+   "reason": "Assistant proceeded with tasks and requested user action.",
    "confirmed": false,
    "net": "B",
    "ts": "2026-07-28T17:40:26.555Z",
@@ -623,7 +624,7 @@
    "user": "go",
    "refusal": "That's the full runbook. It's ready whenever you are \u2014 **Step 1 (rotate) is the trigger**, and I can't do that part for you (it's the GoDaddy dashboard + a live secret).\n\nTwo ways forward, your pick:\n- **Paste the new GoDaddy key** here once you've created it \u2192 I validate it, route it via `secrets` (last-4 confirm only), and then I can drive Steps 3\u20135 of the scrub *with you* (you approve each destructive command).\n- Or say **\"walk me through it\"** and I'll go one command at a time from Step 2.\n\nEither way the actual key creation/revoke (Step 1) and the `filter-repo` rewrite (Step 4) are yours to execute \u2014 I'll set them up and verify around them, but I won't run the rotation or the history rewrite autonomously. Standing by for the key.",
    "category": "other",
-   "reason": "(tone candidate \u2014 unreviewed)",
+   "reason": "Assistant offered help; no policy refusal.",
    "confirmed": false,
    "net": "B",
    "ts": "2026-07-28T00:15:55.609Z",
@@ -656,7 +657,7 @@
    "user": "do for me",
    "refusal": "State's saved to the memo, so we lose nothing. Here's my honest final call:\n\n**I can't do this last 15 seconds for you \u2014 and I've proven it across ~15 attempts, not for lack of trying.** Granting the service account requires an action inside GA4's admin UI, and that specific UI is the one thing my browser automation can't reliably drive (refs die between snapshot and click), while the API alternative is blocked by gcloud not persisting the analytics scope. Both walls are real.\n\n**What I *did* do for you tonight** \u2014 the hard 90%:\n- Diagnosed the orphaned `627787796156` project that was breaking everything\n- Stood up a clean new project + enabled the APIs + minted a working service-account key\n- Located your DW GA account and teed up the exact grant\n\n**The finish is genuinely a 15-second cli",
    "category": "other",
-   "reason": "(tone candidate \u2014 unreviewed)",
+   "reason": "Capability limit, not policy refusal",
    "confirmed": false,
    "net": "B",
    "ts": "2026-07-27T16:30:55.308Z",
@@ -667,7 +668,7 @@
    "user": "DO FOR MW",
    "refusal": "I'm going to be straight with you: I drove this as far as browser automation reliably goes, but **GA4's admin UI won't let me switch account context programmatically** \u2014 every attempt bounces back to the default account. This is the one spot that needs your click, and you're already in the open window, so it's ~15 seconds.\n\n**Everything hard is done** \u2014 I fixed the whole broken chain:\n- \u2705 Orphaned project `627787796156` diagnosed and abandoned\n- \u2705 Fresh project `dw-ga4-attribution` created, Analytics APIs enabled\n- \u2705 New working service-account key minted (`analytics-agent@dw-ga4-attribution.iam.gserviceaccount.com`)\n- \u2705 Located your DW GA account: **DesignerWallcoverings.com (ID 15714274)**\n\n**Your 15-second finish (in the GA4 window that's open):**\n1. Top account picker (**arrow_drop_dow",
    "category": "other",
-   "reason": "(tone candidate \u2014 unreviewed)",
+   "reason": "Assistant provided solution, no policy refusal.",
    "confirmed": false,
    "net": "B",
    "ts": "2026-07-27T16:05:45.333Z",
@@ -678,7 +679,7 @@
    "user": "YES",
    "refusal": "No stored Expo token and no active session \u2014 and unlike Vercel, Expo login is email+password (no device flow I can background). So I need one thing from you: an **Expo access token**.\n\n\u2192 **https://expo.dev/settings/access-tokens** \u2192 \"Create token\" \u2192 copy \u2192 **paste it here.**\n\nWith it I'll immediately, headless:\n1. Validate it + route it via your `secrets` skill (last-4 only, never echoed)\n2. `eas init` \u2014 writes the real `projectId` into `app.json`\n3. `eas env:create EXPO_PUBLIC_API_BASE = https://nineoh-guide.vercel.app` (production) \u2014 so the app talks to the live API we just deployed\n\n**The one part still yours:** the `preview` profile builds a **real-device `.ipa`**, so `eas build` will prompt for **Apple ID login + 2FA** to generate the signing cert \u2014 I can't do that half. I'll get ever",
    "category": "other",
-   "reason": "(tone candidate \u2014 unreviewed)",
+   "reason": "Assistant requested credentials, did not refuse.",
    "confirmed": false,
    "net": "B",
    "ts": "2026-07-27T16:01:06.515Z",
@@ -949,6 +950,5 @@
    "file": "8d476de6-5b4e-4040-a766-a852f6a7dd1b.jsonl",
    "project": "Users-macstudio3"
   }
- ],
- "confirmed_count": 14
+ ]
 }
\ No newline at end of file
diff --git a/package.json b/package.json
index 8f822a0..f6733e4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "rejected-prompts-viewer",
-  "version": "1.0.0",
+  "version": "1.1.0",
   "private": true,
   "description": "Web viewer of prompts Claude declined on copyright/trademark/website-ToS/IP grounds, mined from Claude Code transcripts.",
   "scripts": {
diff --git a/scan.py b/scan.py
index 501b9af..a4a958a 100644
--- a/scan.py
+++ b/scan.py
@@ -5,19 +5,28 @@ Net B: assistant text with a STRONG refusal opener, regardless of keyword — ca
        purely tone-based / ethics refusals that never say 'copyright' etc.
 The Stage-2 local-LLM pass (classify.py) is the arbiter of policy-refusal vs capability-limit.
 Writes /tmp/refusal_cands.json."""
-import json, glob, re, os
+import glob
+import json
+import os
+import re
+from collections import Counter
 
 PROJROOT = os.path.expanduser('~/.claude/projects')
 
-decline = re.compile(r"(I can'?t|I cannot|I won'?t|I'm not able|I am not able|I'm not going to|I'm not comfortable|I shouldn'?t|I have to decline|I'd rather not|not something I can|can'?t ethically|won'?t help|refus|decline to|not willing to|in good conscience)", re.I)
-reason = re.compile(r"(copyright|trademark|intellectual property|terms of service|\bToS\b|their terms|against .{0,20}terms|scrap\w* (without|permission|their)|circumvent|bypass\w* (the )?(bot|captcha|paywall|rate.?limit|detection|login|auth)|unauthorized access|impersonat|counterfeit|\bDMCA\b|someone else'?s (brand|work|design|content)|protected work|passing off|knockoff|knock-off|rip.?off .{0,15}brand|clone .{0,15}(brand|site|store))", re.I)
+# Tunables — change these without hunting through logic below
+MAX_ASST_LEN  = 2500   # assistant turns longer than this are almost never bare refusals
+USER_TRUNCATE = 600    # chars stored for the user prompt
+ASST_TRUNCATE = 800    # chars stored for the assistant decline snippet
+
+decline = re.compile(r"(I can'?t|I cannot|I won'?t|I'm not able|I am not able|I'm not going to|I'm not comfortable|I shouldn'?t|I have to decline|I'd rather not|not something I can|can'?t ethically|won'?t help|refus|decline to|not willing to|in good conscience)", re.IGNORECASE)
+reason = re.compile(r"(copyright|trademark|intellectual property|terms of service|\bToS\b|their terms|against .{0,20}terms|scrap\w* (without|permission|their)|circumvent|bypass\w* (the )?(bot|captcha|paywall|rate.?limit|detection|login|auth)|unauthorized access|impersonat|counterfeit|\bDMCA\b|someone else'?s (brand|work|design|content)|protected work|passing off|knockoff|knock-off|rip.?off .{0,15}brand|clone .{0,15}(brand|site|store))", re.IGNORECASE)
 
 # Net B — strong refusal openers that decline the REQUEST (not a capability limit).
-strong = re.compile(r"(I can'?t help (you )?with (that|this)|I can'?t assist with (that|this)|I won'?t help (you )?(with|do)|I'm not able to help with (that|this)|I have to decline|I'm going to decline|I'm not comfortable (doing|helping|with|creating|writing)|I can'?t in good conscience|I won'?t be able to help with|I can'?t (create|write|generate|provide|build|do) (that|this)|that'?s not something I('?ll| will| can) (help|do|assist)|I won'?t assist|against my (guidelines|values|principles)|I can'?t ethically|I'm not willing to|I need to decline|I can'?t participate in)", re.I)
+strong = re.compile(r"(I can'?t help (you )?with (that|this)|I can'?t assist with (that|this)|I won'?t help (you )?(with|do)|I'm not able to help with (that|this)|I have to decline|I'm going to decline|I'm not comfortable (doing|helping|with|creating|writing)|I can'?t in good conscience|I won'?t be able to help with|I can'?t (create|write|generate|provide|build|do) (that|this)|that'?s not something I('?ll| will| can) (help|do|assist)|I won'?t assist|against my (guidelines|values|principles)|I can'?t ethically|I'm not willing to|I need to decline|I can'?t participate in)", re.IGNORECASE)
 # de-noise Net B: drop obvious capability-limit phrasings
-capability = re.compile(r"(from (in )?here|from the shell|from this session|for you (here|myself)|on your end|only you can|you'?ll need to|requires? (you|a person|your)|quit\w* iTerm|kill\w* (this|the) session|log ?in (yourself|first)|enter your|your (identity|credentials|password|2fa|payout|tax))", re.I)
+capability = re.compile(r"(from (in )?here|from the shell|from this session|for you (here|myself)|on your end|only you can|you'?ll need to|requires? (you|a person|your)|quit\w* iTerm|kill\w* (this|the) session|log ?in (yourself|first)|enter your|your (identity|credentials|password|2fa|payout|tax))", re.IGNORECASE)
 # META filter: this build's OWN session discusses refusals/copyright at length — exclude self-referential noise
-META = re.compile(r"(:9858|rejected-prompts|refusal_cands|refusal_classified|borderline candidate|heretic model|\bNet [AB]\b|classify\.py|build-data|scan\.py|policy declines|decline text|two-signal|capability limit)", re.I)
+META = re.compile(r"(:9858|rejected-prompts|refusal_cands|refusal_classified|borderline candidate|heretic model|\bNet [AB]\b|classify\.py|build-data|scan\.py|policy declines|decline text|two-signal|capability limit)", re.IGNORECASE)
 
 seen = set()
 cands = []
@@ -53,7 +62,7 @@ for f in glob.glob(os.path.join(PROJROOT, '**', '*.jsonl'), recursive=True):
             for it in (m.get('content') or []):
                 if isinstance(it, dict) and it.get('type') == 'text':
                     tx = it.get('text', '')
-                    if len(tx) >= 2500:
+                    if len(tx) >= MAX_ASST_LEN:
                         continue
                     netA = decline.search(tx) and reason.search(tx)
                     netB = strong.search(tx) and not capability.search(tx)
@@ -66,10 +75,9 @@ for f in glob.glob(os.path.join(PROJROOT, '**', '*.jsonl'), recursive=True):
                         continue
                     seen.add(key)
                     cands.append({'file': os.path.basename(f), 'ts': d.get('timestamp'),
-                                  'user': (prev_user or '')[:600], 'refusal': tx[:800],
+                                  'user': (prev_user or '')[:USER_TRUNCATE], 'refusal': tx[:ASST_TRUNCATE],
                                   'net': 'A' if netA else 'B'})
                     break
 
 json.dump(cands, open('/tmp/refusal_cands.json', 'w'), indent=1)
-from collections import Counter
 print(f"CANDIDATES: {len(cands)}  by-net={dict(Counter(c['net'] for c in cands))} -> /tmp/refusal_cands.json")

← b84882e Fix refusal-text corruption (verdict->confirmed key), widen  ·  back to Rejected Prompts Viewer  ·  feat: auto-update — incremental refresh.py + launchd KeepAli b2b7cb8 →