{"success":true,"count":28,"rules":[{"id":1,"category":"VBScript Syntax","ruleNumber":"1.1","title":"Identifiers cannot start with an underscore","symptom":"Compilation error 800a0408 \"Invalid character\" at the line where the identifier appears.","rootCause":"VBScript rejects any identifier whose first character is an underscore. This applies to variables, functions, parameters, and constants.","doPattern":"Dim ansRaw\nFunction fmtTicketPrice(p) : ... : End Function","dontPattern":"Dim _ansRaw\nFunction _fmtPrice(p) : ... : End Function","exampleContext":"Hit in _event_placeholders.asp (renamed _fmtPrice -> fmtTicketPrice and _ordinalSuffix -> ordinalSuffix) and earlier in workshop.asp (_ansRaw).","severity":"critical","tags":"vbscript,syntax,naming","sortOrder":110},{"id":2,"category":"VBScript Syntax","ruleNumber":"1.2","title":"Single-line If/Then cannot be chained with ElseIf","symptom":"Compilation error 800a03f6 \"Expected End\" at the ElseIf line.","rootCause":"A single-line \"If x Then y\" closes the If at end of line. The ElseIf that follows is orphaned. To use ElseIf, every branch must be on its own line with Then at end-of-line, terminated by End If.","doPattern":"If h = 0 Then\n    h = 12\n    ap = \"AM\"\nElseIf h = 12 Then\n    ap = \"PM\"\nElse\n    ap = \"AM\"\nEnd If","dontPattern":"If h = 0 Then h = 12 : ap = \"AM\"\nElseIf h = 12 Then ap = \"PM\"\nElse ap = \"AM\"\nEnd If","exampleContext":"Hit in AIWorkshopOffer.asp FmtTime helper. Note: single-line \"If x Then y Else z\" WITHOUT ElseIf IS legal.","severity":"critical","tags":"vbscript,syntax,control-flow","sortOrder":120},{"id":3,"category":"VBScript Syntax","ruleNumber":"1.3","title":"Functions return by assigning to the function name","symptom":"Either a runtime error or the function returns Empty.","rootCause":"VBScript has no Return keyword. A function returns by assigning its result to a variable with the same name as the function.","doPattern":"Function Double(n)\n    Double = n * 2\nEnd Function","dontPattern":"Function Double(n)\n    Return n * 2\nEnd Function","exampleContext":"Common gotcha when coming from JS or other languages. Every helper in our codebase follows the assign-to-name pattern.","severity":"critical","tags":"vbscript,syntax,function","sortOrder":130},{"id":4,"category":"VBScript Syntax","ruleNumber":"1.4","title":"VBScript has no native IIf — define your own","symptom":"Runtime error 800a000d \"Type mismatch: IIf\".","rootCause":"Unlike VB/VBA, VBScript does not ship with an IIf function. Calling IIf without a local definition throws Type mismatch at runtime.","doPattern":"Function IIf(cond, valT, valF)\n    If cond Then IIf = valT Else IIf = valF\nEnd Function","dontPattern":"x = IIf(cond, valTrue, valFalse)   'without a local IIf definition","exampleContext":"Hit in Board.asp. Defined a local IIf helper at the top of the file to fix it.","severity":"critical","tags":"vbscript,syntax,iif","sortOrder":140},{"id":5,"category":"Null Handling","ruleNumber":"2.1","title":"Null & \"\" = Null (not \"\")","symptom":"Downstream string operations like Len, Left, Mid, InStr throw Type mismatch.","rootCause":"VBScript preserves Null through string concatenation. Concatenating Null with \"\" does NOT coerce to \"\". You must explicitly check with IsNull.","doPattern":"Dim x : x = dict(key) & \"\"\nIf IsNull(x) Then x = \"\"","dontPattern":"Dim x : x = dict(key)\nIf Len(x) > 0 Then ...   'throws Type mismatch if x is Null","exampleContext":"Hit multiple times in workshop.asp (pills_multi block) and in _event_placeholders.asp.","severity":"critical","tags":"null,vbscript,string","sortOrder":210},{"id":6,"category":"Null Handling","ruleNumber":"2.2","title":"Always coerce recordset field reads with & \"\"","symptom":"Any NULL column poisons downstream concat/string ops.","rootCause":"Reading rs(\"FieldName\") returns a Variant. NULL columns come back as the Null value, not an empty string. Append & \"\" to coerce to String.","doPattern":"evName = rs(\"EventName\") & \"\"","dontPattern":"evName = rs(\"EventName\")   'fragile — breaks if EventName is NULL","exampleContext":"Standard pattern across all our .asp files. Every single rs(...) read in GetEventDetail.asp, SaveEvent.asp, etc. uses & \"\".","severity":"critical","tags":"null,recordset,database","sortOrder":220},{"id":7,"category":"Null Handling","ruleNumber":"2.3","title":"Mid/Left/InStr on non-string Variants throws Type mismatch","symptom":"Runtime error 800a000d \"Type mismatch\" on the Mid/Left/InStr call.","rootCause":"Dictionary returns from NTEXT columns or certain Variant types pass IsNull/IsEmpty but still blow up on string ops. Coerce defensively with & \"\" AND wrap in On Error Resume Next.","doPattern":"On Error Resume Next\nDim raw : raw = existing & \"\"\nIf IsNull(raw) Then raw = \"\"\nIf Len(raw) > 0 Then pipeP = InStr(1, raw, \"|\", 1)\nIf Err.Number <> 0 Then Err.Clear : raw = \"\"\nOn Error GoTo 0","dontPattern":"pipeP = InStr(1, existing, \"|\", 1)   'throws if existing is a non-string Variant","exampleContext":"Hit multiple times in workshop.asp pills_multi block (line 556). The dictionary returned NTEXT content as an odd Variant.","severity":"critical","tags":"null,vbscript,string,dictionary","sortOrder":230},{"id":8,"category":"Null Handling","ruleNumber":"2.4","title":"CDate can throw Type mismatch even when IsDate is True","symptom":"Runtime error 800a000d \"Type mismatch\" on WeekdayName/Weekday/MonthName after a CDate.","rootCause":"Some DB date formats pass IsDate() but fail CDate() downstream (localization quirks, odd formats). Wrap casts in On Error Resume Next.","doPattern":"Dim dCast\nOn Error Resume Next\nIf IsDate(evDate) Then\n    dCast = CDate(evDate)\n    If Err.Number = 0 Then\n        dateLong = WeekdayName(Weekday(dCast), False) & \", \" & MonthName(Month(dCast), False) & \" \" & Day(dCast) & \", \" & Year(dCast)\n    End If\nEnd If\nIf Err.Number <> 0 Then Err.Clear\nOn Error GoTo 0","dontPattern":"dateLong = WeekdayName(Weekday(CDate(evDate)), False) & \" \" & ...   'no guard","exampleContext":"Hit in AIWorkshopOffer.asp line 105. IsDate(evDate) returned True but the chained CDate/Weekday call threw Type mismatch anyway.","severity":"critical","tags":"date,cdate,vbscript","sortOrder":240},{"id":9,"category":"Null Handling","ruleNumber":"2.5","title":"Read migration-added columns defensively","symptom":"Runtime error \"Item cannot be found in the collection corresponding to the requested name\" — often crashes the page in dev/staging where the migration hasn't run yet.","rootCause":"When a new column is added via migration, other environments may not have it yet. Always wrap reads in On Error Resume Next until the migration is universally deployed.","doPattern":"Dim evSub : evSub = \"\"\nOn Error Resume Next\nevSub = rs(\"Subtitle\") & \"\"\nIf Err.Number <> 0 Then evSub = \"\" : Err.Clear\nOn Error GoTo 0","dontPattern":"evSub = rs(\"Subtitle\") & \"\"   'crashes if the Subtitle column migration hasn't run yet","exampleContext":"Pattern used in GetEventDetail.asp for the new Subtitle column (added 2026-04-29).","severity":"warning","tags":"migration,schema,defensive","sortOrder":250},{"id":10,"category":"Database","ruleNumber":"3.1","title":"MARS deadlock — one open recordset per connection","symptom":"Queries silently return nothing, or the page renders empty JSON. No error is thrown.","rootCause":"SQL Server without MARS allows only one open recordset per connection. Calling a helper that opens its own recordset while a parent rs is still open deadlocks or returns empty.","doPattern":"rawDesc = rs(\"Description\") & \"\"\njsonOut = jsonOut & \"\"\"description\"\":\"\"__DESC_PLACEHOLDER__\"\",\"\nrs.Close : Set rs = Nothing\n'Now safe to call helpers that open their own recordsets:\nDim resolved : resolved = ResolveEventPlaceholders(rawDesc, id, conn)\njsonOut = Replace(jsonOut, \"__DESC_PLACEHOLDER__\", JSONEsc(resolved))","dontPattern":"Set rs = conn.Execute(\"SELECT ...\")\nresolved = ResolveEventPlaceholders(rs(\"Description\") & \"\", id, conn)   'BREAKS — rs still open","exampleContext":"Hit in GetEventDetail.asp — EditEvent.asp and Eventdetails.asp were loading blank because ResolveEventPlaceholders was called while rs was still open. Fix: capture raw, close rs, call helper, stitch result back with Replace.","severity":"critical","tags":"database,mars,recordset,sqlserver","sortOrder":310},{"id":11,"category":"Database","ruleNumber":"3.2","title":"Always close recordsets and null the reference","symptom":"Connection pool leaks, slow queries over time, occasional \"connection busy\" errors.","rootCause":"ADO recordsets hold connection resources. Close + Set Nothing frees them immediately instead of waiting for GC.","doPattern":"rs.Close : Set rs = Nothing","dontPattern":"'...no close... just let the rs go out of scope","exampleContext":"Every recordset read in the codebase should be followed by this pair.","severity":"warning","tags":"database,recordset,cleanup","sortOrder":320},{"id":12,"category":"Database","ruleNumber":"3.3","title":"Always SQL-escape user values before building SQL","symptom":"SQL injection vulnerability + runtime SQL syntax errors on values containing apostrophes.","rootCause":"Build a SqlSafe helper that doubles single quotes. Use it on every value that goes into a SQL string.","doPattern":"Function SqlSafe(str) : SqlSafe = Replace(str & \"\", \"'\", \"''\") : End Function\nsql = \"UPDATE BN_Events SET EventName = '\" & SqlSafe(evName) & \"' WHERE ...\"","dontPattern":"sql = \"UPDATE BN_Events SET EventName = '\" & evName & \"' WHERE ...\"   'unsanitized","exampleContext":"SqlSafe is defined in SaveEvent.asp and used across all our UPDATE/INSERT statements.","severity":"critical","tags":"database,security,sql-injection","sortOrder":330},{"id":13,"category":"Database","ruleNumber":"3.4","title":"Clamp form input lengths to column width before SQL","symptom":"SQL error \"String or binary data would be truncated\" when a user pastes something longer than the column width.","rootCause":"Before using a form value in SQL, check Len() and truncate with Left() to match the column definition.","doPattern":"If Len(evSubtitle) > 255 Then evSubtitle = Left(evSubtitle, 255)","dontPattern":"'no length check before UPDATE BN_Events SET Subtitle = '...'","exampleContext":"Pattern used in SaveEvent.asp for the Subtitle NVARCHAR(255) column.","severity":"warning","tags":"database,validation,input","sortOrder":340},{"id":14,"category":"Database","ruleNumber":"3.5","title":"Migrations must be idempotent","symptom":"Re-running a migration throws \"column already exists\" or duplicates seed data.","rootCause":"Every schema change must be guarded by IF NOT EXISTS / IF EXISTS. Every seed UPDATE should be guarded by a WHERE that only matches when the field is currently empty/null.","doPattern":"IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID('dbo.BN_Events') AND name = 'Subtitle')\nBEGIN\n    ALTER TABLE dbo.BN_Events ADD Subtitle NVARCHAR(255) NULL\nEND","dontPattern":"ALTER TABLE dbo.BN_Events ADD Subtitle NVARCHAR(255) NULL   'fails on re-run","exampleContext":"Pattern used in _migrations/2026-04-29_event_subtitle.sql.","severity":"warning","tags":"database,migration,idempotent","sortOrder":350},{"id":28,"category":"Database","ruleNumber":"3.6","title":"Admin check on BN_Members is MemberRole='admin', NOT a boolean IsAdmin column","symptom":"SQL error 80040e14 \"Invalid column name 'IsAdmin'\" when querying BN_Members for an admin check.","rootCause":"The BN_Members table uses a text MemberRole column (values: \"member\", \"admin\", possibly others) - there is no IsAdmin bit column. Don't invent one. Every page that needs to gate on admin status must read MemberRole.","doPattern":"Set rs = conn.Execute(\"SELECT ISNULL(MemberRole,'member') AS MemberRole FROM BN_Members WHERE MemberID = \" & CLng(mid))\nIf Not rs.EOF Then isAdmin = (LCase(Trim(rs(\"MemberRole\") & \"\")) = \"admin\")","dontPattern":"Set rs = conn.Execute(\"SELECT ISNULL(IsAdmin,0) AS IsAdmin FROM BN_Members ...\")   'column does not exist","exampleContext":"Hit on MemberVlog.asp, AdminVlog.asp, Vlog_api.asp on 2026-04-29. The canonical pattern lives in Shoutouts.asp lines 10-21.","severity":"critical","tags":"database,schema,admin,members,bn_members","sortOrder":360},{"id":15,"category":"File Organization","ruleNumber":"4.1","title":"Define helper Functions at the TOP of the ASP file","symptom":"Inconsistent behavior — the function works in some places but returns Empty in others.","rootCause":"VBScript function hoisting is reliable within a single <% %> block but inconsistent when functions are defined in one block and called from HTML-inline <%= ... %> blocks lower in the file. Putting helpers in the first <% %> block avoids this.","doPattern":"<%@ Language=VBScript %>\n<%\nResponse.Buffer = True\n%>\n<!--#include file=\"Dbconn_bn.asp\"-->\n<%\n'ALL HELPERS HERE:\nFunction HE(s) : ... : End Function\nFunction JSReady(s) : ... : End Function\n'...then data loading, then render\n%>","dontPattern":"'helpers defined near the BOTTOM of the file, called from inline <%= ... %> at the top","exampleContext":"Hit in EventSignup.asp — JSReadyName was defined at the bottom but called in a <script> block higher up. Moved to the top helpers section.","severity":"warning","tags":"file-organization,hoisting","sortOrder":410},{"id":16,"category":"File Organization","ruleNumber":"4.2","title":"Never use <!--#include--> inside a conditional","symptom":"The included file loads unconditionally regardless of the runtime branch.","rootCause":"<!--#include--> is a compile-time directive, inlined before VBScript runs. For runtime-conditional includes, use Server.Execute.","doPattern":"If role = \"admin\" Then Server.Execute \"admin_sidebar.asp\" Else Server.Execute \"member_sidebar.asp\"","dontPattern":"If role = \"admin\" Then\n    <!--#include file=\"admin_sidebar.asp\"-->   'always included regardless of role","exampleContext":"Pattern used in Board.asp and workshop.asp for role-based sidebars.","severity":"warning","tags":"file-organization,include","sortOrder":420},{"id":17,"category":"User Input","ruleNumber":"5.1","title":"Coerce every form value with & \"\" before using","symptom":"Request.Form returns a Variant. Using it raw can trigger Type mismatch on Trim/Len/concat.","rootCause":"Append & \"\" to force a String, then Trim. This is the standard first line for every form read.","doPattern":"action = Trim(Request.Form(\"action\") & \"\")\nevId   = Trim(Request.Form(\"eventID\") & \"\")","dontPattern":"action = Request.Form(\"action\")\nevId   = Request.Form(\"eventID\")","exampleContext":"Standard across all our form-handling endpoints (SaveEvent.asp, RSVPEvent.asp, GuestSignup.asp, etc.).","severity":"warning","tags":"input,form,coerce","sortOrder":510},{"id":18,"category":"User Input","ruleNumber":"5.2","title":"Validate numeric input with IsNumeric before casting","symptom":"Runtime \"Type mismatch\" on CLng/CInt/CDbl when the value isn't a clean number.","rootCause":"Check IsNumeric first, fail early with a helpful message if not.","doPattern":"If evId = \"\" Or Not IsNumeric(evId) Then Fail \"Invalid event ID\"\neventID = CLng(evId)","dontPattern":"eventID = CLng(Request.Form(\"eventID\"))   'throws if missing or non-numeric","exampleContext":"Used in SaveEvent.asp, RSVPEvent.asp delete/update flows.","severity":"critical","tags":"input,validation,numeric","sortOrder":520},{"id":19,"category":"User Input","ruleNumber":"5.3","title":"Multi-valued checkboxes join with \", \" automatically","symptom":"Developers coming from PHP try to use name=\"q[]\" which Classic ASP does not understand.","rootCause":"Classic ASP joins same-named form fields with a literal \", \" separator. No [] suffix. Use Split(value, \", \") to parse.","doPattern":"<input type=\"checkbox\" name=\"q_123\" value=\"A\">\n<input type=\"checkbox\" name=\"q_123\" value=\"B\">\n<%\n'Request.Form(\"q_123\") returns \"A, B\"\nselections = Split(Request.Form(\"q_123\") & \"\", \", \")\n%>","dontPattern":"<input type=\"checkbox\" name=\"q_123[]\" value=\"A\">   'PHP convention; wrong for ASP","exampleContext":"Pattern used in workshop.asp pills_multi blocks.","severity":"best-practice","tags":"input,form,checkbox","sortOrder":530},{"id":20,"category":"LLM APIs","ruleNumber":"6.1","title":"Use ===SECTION=== delimiters, not nested JSON-in-string","symptom":"AI response parse repeatedly fails with \"AI response parse failed (HTTP 200)\". Character-by-character state machines to unwrap nested JSON are fragile in VBScript.","rootCause":"Tell the model to output plain text with explicit delimiters. In VBScript, find sections with InStr + Mid.","doPattern":"'Prompt the model:\n\"===SUBJECT===\" + CHAR(10) + \"Internal label.\" + CHAR(10) + \"===BODY===\" + CHAR(10) + \"The message.\" + CHAR(10) + \"===END===\"\n'Parse:\nsecSubject = SecBetween(raw, \"===SUBJECT===\", \"===BODY===\")","dontPattern":"'Ask the model to output nested JSON like {\"subject\":\"...\",\"body\":\"...\"} then wrestle with escaping","exampleContext":"Switched all AI endpoints to delimiter format: EmailMarketing_aigen.asp, SMSMarketing_aigen.asp, Workshop_website_review.asp. Fixed months of parsing headaches.","severity":"critical","tags":"ai,llm,parsing,gemini","sortOrder":610},{"id":21,"category":"LLM APIs","ruleNumber":"6.2","title":"Set maxOutputTokens generously (8192+)","symptom":"Parse succeeds on test prompts but fails in prod — response looks \"empty\" because it was truncated mid-output.","rootCause":"Low maxOutputTokens truncates long responses silently (no error, just cut off). Landing pages, email copy, and multi-section prompts need room.","doPattern":"\"generationConfig\":{\"temperature\":0.6,\"maxOutputTokens\":8192}","dontPattern":"\"generationConfig\":{\"temperature\":0.6,\"maxOutputTokens\":1200}   'truncates anything substantial","exampleContext":"Had to bump limits in Workshop_website_review.asp from 1200 -> 8192.","severity":"warning","tags":"ai,llm,tokens,gemini","sortOrder":620},{"id":22,"category":"LLM APIs","ruleNumber":"6.3","title":"In the system prompt, say \"no JSON, no markdown, no preamble\"","symptom":"Model wraps the output in ```markdown fences``` or adds a \"Sure, here is your response:\" preamble that breaks the delimiter parser.","rootCause":"Be explicit in the system prompt about output format constraints. The model follows instructions better with negative examples.","doPattern":"\"Output a SINGLE plain-text format with EXACT delimiters below. No JSON, no markdown, no preamble.\"","dontPattern":"'no format constraints in the system prompt — hope the model gets it right","exampleContext":"System prompts in all our AI endpoints start with a strict format rules block.","severity":"warning","tags":"ai,llm,prompting","sortOrder":630},{"id":23,"category":"HTML Rendering","ruleNumber":"7.1","title":"Always HTMLEncode user-provided values","symptom":"XSS vulnerability + broken rendering when names contain & < > \".","rootCause":"Use Server.HTMLEncode (or a wrapped helper) on every user value that hits HTML. Wrapper handles Null safely.","doPattern":"Function HE(s) : If IsNull(s) Then HE = \"\" Else HE = Server.HTMLEncode(s & \"\") : End Function\n<div><%= HE(evName) %></div>","dontPattern":"<div><%= evName %></div>   'raw concat — XSS","exampleContext":"HE helper is standard at the top of every new .asp file.","severity":"critical","tags":"html,xss,escape","sortOrder":710},{"id":24,"category":"HTML Rendering","ruleNumber":"7.2","title":"Use a separate attribute-safe escape for HTML attributes","symptom":"Broken attributes when values contain double-quotes.","rootCause":"HTMLEncode is for element text. Attribute values need HA() that escapes & \" < (and ideally ' too).","doPattern":"Function HA(s)\n    If IsNull(s) Then HA = \"\" : Exit Function\n    HA = Replace(Replace(Replace(s & \"\", \"&\", \"&amp;\"), \"\"\"\", \"&quot;\"), \"<\", \"&lt;\")\nEnd Function\n<img alt=\"<%= HA(evName) %>\" src=\"<%= HA(evImage) %>\">","dontPattern":"<img alt=\"<%= HE(evName) %>\" src=\"<%= HE(evImage) %>\">   'HE alone may not escape all attribute-unsafe chars consistently","exampleContext":"Used in AIWorkshopOffer.asp, EventSignup.asp.","severity":"warning","tags":"html,escape,attribute","sortOrder":720},{"id":25,"category":"HTML Rendering","ruleNumber":"7.3","title":"JS-safe inline needs a JSReady helper","symptom":"Script errors when a VBScript string with quotes or backslashes is inlined into a <script> block.","rootCause":"When writing VBScript data into a JS string literal, escape \\, \", and newlines.","doPattern":"Function JSReady(s)\n    If IsNull(s) Then JSReady = \"\" : Exit Function\n    Dim t : t = s & \"\"\n    t = Replace(t, \"\\\", \"\\\\\")\n    t = Replace(t, \"\"\"\", \"\\\"\"\")\n    t = Replace(t, Chr(10), \" \")\n    t = Replace(t, Chr(13), \" \")\n    JSReady = t\nEnd Function\n<script>var name = \"<%= JSReady(prefName) %>\";</script>","dontPattern":"<script>var name = \"<%= prefName %>\";</script>   'any quote in the name kills the page","exampleContext":"Used in EventSignup.asp for prefilling name from cookie.","severity":"warning","tags":"html,javascript,escape","sortOrder":730},{"id":26,"category":"Cookies & Auth","ruleNumber":"8.1","title":"Request.Cookies — coerce with & \"\" and always validate","symptom":"Missing cookies return an empty Variant that trips type ops. Client-controlled values can inject SQL.","rootCause":"Always coerce to string, Trim, AND validate (numeric check, membership lookup, etc.) before using in SQL or logic.","doPattern":"existingMid = Trim(Request.Cookies(\"sde_mid\") & \"\")\nIf existingMid <> \"\" And IsNumeric(existingMid) Then\n    'safe to use CLng(existingMid) now\nEnd If","dontPattern":"mid = Request.Cookies(\"sde_mid\")\nsql = \"... WHERE MemberID = \" & mid","exampleContext":"Used in EventSignup.asp, Memberdashboard.asp, ScanConnect.asp, MyProspects.asp.","severity":"critical","tags":"cookie,auth,security","sortOrder":810},{"id":27,"category":"Error Handling","ruleNumber":"9.1","title":"Master defensive pattern — never let data crash the page","symptom":"A data anomaly (NULL, odd date format, missing column) 500s the whole page instead of rendering a graceful fallback.","rootCause":"Wrap any operation that could throw (CDate, risky field reads, string ops on Variants) in On Error Resume Next, check Err.Number, fall back to a sensible default, Err.Clear, On Error GoTo 0.","doPattern":"On Error Resume Next\nDim result : result = \"\"\nresult = risky_operation()\nIf Err.Number <> 0 Then\n    result = \"\"\n    Err.Clear\nEnd If\nOn Error GoTo 0","dontPattern":"'call risky_operation() with no guards","exampleContext":"Used around CDate in AIWorkshopOffer.asp and EventSignup.asp; around Mid/Left/InStr in workshop.asp; around rs(col) for new migration columns.","severity":"critical","tags":"error-handling,defensive","sortOrder":910}]}