{"success":true,"count":55,"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":29,"category":"VBScript Syntax","ruleNumber":"1.5","title":"Never Dim the same variable twice in one Sub/Function","symptom":"Compilation error 800a0411 \"Name redefined\" at the second Dim line. The whole file fails to compile so the page 500s.","rootCause":"VBScript treats a duplicate Dim inside the same procedure scope as a hard compile error. Most other languages let you re-declare; VBScript does not. Easy to hit when you copy-paste a block from elsewhere in the file and the variable was already Dim'd earlier in the same Sub.","doPattern":"Sub sendThemedConfirmation()\n    Dim host : host = Request.ServerVariables(\"HTTP_HOST\")\n    ' ... 30 lines later ...\n    host = Request.ServerVariables(\"HTTP_HOST\")   ' just assign, do not re-Dim\nEnd Sub","dontPattern":"Sub sendThemedConfirmation()\n    Dim host : host = Request.ServerVariables(\"HTTP_HOST\")\n    ' ... 30 lines later ...\n    Dim host   ' second Dim — compile error\nEnd Sub","exampleContext":"Hit in request_submit.asp on 2026-05 — Dim host appeared at lines 542 and 574 inside the same Sub. Removed the second Dim and the file compiled.","severity":"critical","tags":"vbscript, syntax, dim, scope, compile","sortOrder":150},{"id":30,"category":"VBScript Syntax","ruleNumber":"1.6","title":"Use CDbl/CCur for large numbers — CLng overflows at ~2.1 billion","symptom":"Runtime error 800a0006 \"Overflow\" on CLng() when the input number is above 2,147,483,647 (or below -2,147,483,648).","rootCause":"CLng casts to a 32-bit signed integer. Phone numbers, Stripe amounts in cents, Twilio IDs, large timestamps and many other \"numeric-looking\" strings exceed Long range and overflow. CDbl handles any size; CCur is correct for currency math.","doPattern":"If IsNumeric(rawAmount) Then\n    amount = CDbl(rawAmount)        ' arbitrary magnitude, OK for IDs\n    ' or, for money math:\n    amount = CCur(rawAmount)        ' fixed 4-decimal currency type\nEnd If","dontPattern":"amount = CLng(Request.Form(\"amount_cents\"))   ' overflows on anything >= $21.47M\nphoneInt = CLng(rawPhone)                       ' overflows on most full-format phone numbers","exampleContext":"Hit in CPG property edit pricing on 2026-05. Default to CDbl unless you specifically need 32-bit truncation.","severity":"critical","tags":"vbscript, syntax, clng, overflow, casting, numeric","sortOrder":160},{"id":41,"category":"VBScript Syntax","ruleNumber":"1.7","title":"Don't colon-pack If/Else across a function body","symptom":"Compilation error 800a03f4 'Expected If' or 800a0401 'Expected end of statement' when the page loads — sometimes only on IIS, not when you eyeball the file.","rootCause":"VBScript's colon separator only works for the SINGLE-LINE If syntax (If x Then a = 1 Else a = 2). The moment you try to wrap a multi-keyword block (Function, Sub, Do, For) with colons, the parser can't tell where the block ends. Pasting from one-line examples is the common trigger.","doPattern":"Function HE(s)\n    If IsNull(s) Then\n        HE = \"\"\n    Else\n        HE = Server.HTMLEncode(s & \"\")\n    End If\nEnd Function","dontPattern":"Function HE(s) : If IsNull(s) Then HE = \"\" Else HE = Server.HTMLEncode(s & \"\") : End Function","exampleContext":"ManageCodingRules.asp top-of-file helpers. Multiple ASP files across BN + freestylerslegacy.","severity":"critical","tags":"vbscript, syntax, function, colons, parser","sortOrder":170},{"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":42,"category":"Null Handling","ruleNumber":"2.6","title":"Always wrap recordset values with HE() or HA() before output","symptom":"Type mismatch 'CStr' (800a000d) when a column is NULL. Or worse — silent <script> injection from a user-supplied field rendered raw.","rootCause":"rs(\"col\") returns Null (not empty string) when the column is NULL. Server.HTMLEncode(Null) throws. Concatenating Null with anything but & \"\" propagates Null. And raw output of any DB string is an XSS vector if the value ever came from a user form. The HE()/HA() helpers handle both problems in one call.","doPattern":"<%= HE(rs(\"first_name\")) %>\n<input value=\"<%= HA(rs(\"title\")) %>\">\n\n' Top-of-file helpers (multi-line — see rule 1.7):\nFunction HE(s)\n    If IsNull(s) Then\n        HE = \"\"\n    Else\n        HE = Server.HTMLEncode(s & \"\")\n    End If\nEnd Function\nFunction HA(s)\n    If IsNull(s) Then HA = \"\" : Exit Function\n    HA = Replace(Replace(Replace(s & \"\", \"&\", \"&amp;\"), \"\"\"\", \"&quot;\"), \"<\", \"&lt;\")\nEnd Function","dontPattern":"<%= rs(\"first_name\") %>\n<input value=\"<%= rs(\"title\") %>\">","exampleContext":"Standard top-of-file helpers in every BN admin page (ManageCodingRules.asp, editinvoice.asp).","severity":"critical","tags":"vbscript, null, htmlencode, xss, helpers","sortOrder":260},{"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":31,"category":"Database","ruleNumber":"3.7","title":"Never SELECT NVARCHAR(MAX) columns alongside short fields you actually need to read","symptom":"Short text and numeric fields silently come back as NULL on the SQLOLEDB driver when a NVARCHAR(MAX) (or NTEXT) column is in the same recordset. No error is thrown — the data just disappears. The standard \"adUseClient + adOpenStatic\" workaround does NOT always fix it on Windows Server.","rootCause":"Once a MAX column is in the SELECT, the default forward-only cursor streams that column as a chunked LOB and the driver loses track of values for other columns when they're read in a different order. Pulling MAX columns in isolation is the only reliable fix we've found.","doPattern":"' 1) Read short fields without the MAX columns:\nSet rs = conn.Execute(\"SELECT PropertyID, PropertyName, Price, LeasePrice FROM CPG_Properties WHERE ...\")\n' 2) Then, for each row that needs them, pull MAX columns in isolation:\nFunction DirectGet(propID, colName)\n    Dim rsX\n    Set rsX = conn.Execute(\"SELECT [\" & colName & \"] FROM CPG_Properties WHERE PropertyID = \" & CLng(propID))\n    If Not rsX.EOF Then DirectGet = rsX(0) & \"\" Else DirectGet = \"\"\n    rsX.Close : Set rsX = Nothing\nEnd Function","dontPattern":"Set rs = conn.Execute(\"SELECT PropertyID, PropertyName, Price, LeasePrice, Description, Features, AIMarketOpinion FROM CPG_Properties WHERE ...\")\n' rs(\"Price\") and rs(\"LeasePrice\") will silently be NULL because Description/Features/AIMarketOpinion are NVARCHAR(MAX)","exampleContext":"Hit hard in CPG_Properties admin on 2026-05 after adding Description / Features / LeasePriceLines / AIMarketOpinion as NVARCHAR(MAX). Brochure + listing + admin form were all reading NULL for Price.","severity":"critical","tags":"database, ado, nvarchar, max, cursor, sqloledb","sortOrder":370},{"id":32,"category":"Database","ruleNumber":"3.8","title":"Single-field edits need a surgical UPDATE action — never reuse the bulk update path","symptom":"Editing one field through a UI silently wipes other columns to NULL/empty because the bulk update action expects every column in the POST and treats missing ones as blanks.","rootCause":"A bulk update handler typically does \"UPDATE T SET col1=?, col2=?, col3=? ...\" with every form value, including the ones that weren't in the lightweight UI. If you call it from a one-field editor (like an inline title edit), the omitted fields land as empty strings or NULL.","doPattern":"' Add a surgical action that only UPDATEs the one column:\nCase \"update_title\"\n    sql = \"UPDATE FL_Photos SET Title = ? WHERE PhotoID = ? AND OwnerRiderID = ?\"\n    ' (parameterized; touches nothing else)\nEnd Case","dontPattern":"' Inline title editor sends only {action:\"update\", title:\"new\"}\n' Server runs the bulk UPDATE — wipes caption, year, location, category, spotId, etc.","exampleContext":"Hit in photo_api.asp on 2026-05 — the inline lightbox title edit was using action=update and nulling out category/spotId/jamId/compId. Added an update_title action that touches only Title.","severity":"warning","tags":"database, update, partial, ux, api","sortOrder":380},{"id":33,"category":"Database","ruleNumber":"3.9","title":"Auto-create-on-first-hit table patterns fail silently — always verify with a SELECT","symptom":"Page tracking / logging / stats appear \"broken\" — counts stay at zero forever. No errors visible in the UI because the CREATE TABLE is wrapped in On Error Resume Next and the INSERT silently no-ops when the table doesn't exist.","rootCause":"IIS app pool DB user often lacks CREATE TABLE permission in production. The standard \"IF NOT EXISTS / CREATE TABLE\" idiom wrapped in OERN swallows the permission error, then every subsequent INSERT is a silent NOOP against a non-existent object.","doPattern":"' Run the CREATE TABLE manually as a privileged user the FIRST time:\n' SQL_Create_VlogPageViews.sql — checked into _migrations/ and executed in SSMS.\n' Then ship a one-off diagnostic page (VlogTrackingDiag.asp) that prints sysobjects/CREATE/INSERT errors verbatim so you can debug permission issues directly.","dontPattern":"' Just hope the auto-create worked:\nOn Error Resume Next\nconn.Execute \"IF NOT EXISTS (...) CREATE TABLE BN_VlogPageViews (...)\"\nconn.Execute \"INSERT INTO BN_VlogPageViews ...\"\n' (errors are eaten — you'll never know why nothing is logging)","exampleContext":"Hit on BN_VlogPageViews 2026-05 — auto-create failed silently in prod (Appdata user lacked CREATE TABLE), so every page view INSERT was a NOOP and stat pills showed 0.","severity":"warning","tags":"database, ddl, permissions, migration, logging","sortOrder":390},{"id":43,"category":"Database","ruleNumber":"3.10","title":"Wrap every conn.Execute in On Error Resume Next + JSON error reply","symptom":"Browser sees Server 500 with the raw \"Microsoft OLE DB Provider for SQL Server error '80040e14'\" page. Front-end .catch() handlers fire with a generic \"Network error\" toast because the response isn't JSON.","rootCause":"Classic ASP without On Error Resume Next bubbles the SQL Server error straight to IIS as a 500 with a leaked HTML error page. Front-end fetch().then(r => r.json()) blows up because the body is HTML, not JSON, and the user gets no actionable error.","doPattern":"On Error Resume Next\nSet rs = conn.Execute(sql)\nIf Err.Number <> 0 Then\n    Response.Clear\n    Response.ContentType = \"application/json\"\n    Response.Write \"{\"\"success\"\":false,\"\"error\"\":\"\"DB error (\" & action & \"): \" & _\n        Replace(Err.Description, \"\"\"\", \"'\") & \"\"\"}\"\n    Err.Clear\n    Response.End\nEnd If\nOn Error GoTo 0","dontPattern":"Set rs = conn.Execute(sql)\n' …code that assumes rs is valid…","exampleContext":"TeamAdmin_api.asp send_invoice + send_w9 + show_times. Pattern now standard across the file.","severity":"critical","tags":"sql, error-handling, ajax, json, 500, oledb","sortOrder":400},{"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":44,"category":"Database","ruleNumber":"3.11","title":"Grep the codebase for table/column names before writing a new query","symptom":"SQL Server error 80040e14 'Invalid object name' or 'Invalid column name' on a query you just wrote — even though sibling pages query the same data fine.","rootCause":"Table names get pluralized/abbreviated differently across the schema (services vs service_lookup, accounts vs Accounts, riders.last_name doesn't exist — it's just .name). The only reliable check is to grep the existing codebase for any other file that already joins or selects from the same data and copy that exact shape.","doPattern":"-- Confirmed by greping editinvoice.asp + bonus.asp + PendingApprovalBlast.asp first:\nJOIN service_lookup sv ON sv.service_code = s.service_code","dontPattern":"-- Assumed name based on what \"seems normal\":\nJOIN services sv ON sv.service_id = s.service_id","exampleContext":"TeamAdmin_api.asp line 548 send_invoice action. Verified against TeamAdmin.asp line 783.","severity":"critical","tags":"sql, schema, joins, oledb, 80040e14","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":45,"category":"Database","ruleNumber":"3.12","title":"Every transactional SendGrid email must write a row to EmailsLog","symptom":"A customer claims they never got the refund/W9/invoice email — and there's no record of whether SendGrid accepted it, what the subject was, or where it was sent.","rootCause":"Each project starts treating EmailsLog as \"the email blast log\" and writes only campaign mail to it. Then refunds, W9s, invoices, password resets etc. ship without logging — so when a customer disputes a send, you can't even prove the attempt was made.","doPattern":"objHttp.Send sendgridJson\nLogRefundEmail rs(\"InvoiceID\"), recipient, name, sendStatus, errMsg, subject, body\n\nSub LogRefundEmail(invoiceID, addr, nm, status, errMsg, subj, htmlBody)\n    Dim sql\n    sql = \"INSERT INTO EmailsLog (EmailAddress, Name, SentDate, Status, ErrorMessage, \" & _\n          \"EmailSentTypeID, subject, htmlbody, emailsentto) VALUES (\" & _\n          \"'\" & Replace(addr, \"'\", \"''\") & \"','\" & Replace(nm, \"'\", \"''\") & \"', GETDATE(), \" & _\n          \"'\" & status & \"', '\" & Replace(errMsg, \"'\", \"''\") & \"', 20, \" & _\n          \"'\" & Replace(subj, \"'\", \"''\") & \"', '\" & Replace(htmlBody, \"'\", \"''\") & \"', \" & _\n          \"'\" & Replace(addr, \"'\", \"''\") & \"')\"\n    conn.Execute sql\nEnd Sub","dontPattern":"objHttp.Send sendgridJson\n' …no logging…","exampleContext":"BMXRefund.asp. Refund check #20 EmailSentTypeID — add to EmailSentTypes lookup.","severity":"best-practice","tags":"email, sendgrid, logging, audit, emailslog","sortOrder":420},{"id":34,"category":"File Organization","ruleNumber":"4.3","title":"Use #include virtual=\"/path\" for shared config — never duplicate Const declarations","symptom":"Compilation error 800a0411 \"Name redefined\" on a Const line, because the same Const is declared in two files included in the same compile pass (or one inline + one in an include).","rootCause":"Classic ASP doesn't allow \"..\" in #include file= paths, which tempts people to paste the same Const block into every subfolder. The moment one of those subfolder files also includes a shared config, you get a duplicate Const collision. The fix is one canonical Const per project, referenced via #include virtual=\"/path/from/webroot.asp\" from anywhere.","doPattern":"<!--#include virtual=\"/gemini_config.asp\"-->\n<%\n' That include is the ONLY place GEMINI_API_URL is declared.\n' If you need a different URL in one file, name it differently or alias it locally.\n%>","dontPattern":"' compose_email.asp (inline):\nConst GEMINI_API_URL = \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent\"\n<!--#include virtual=\"/gemini_config.asp\"-->   ' also declares Const GEMINI_API_URL\n' ? \"Name redefined\" at the inline Const","exampleContext":"Hit on 2026-05 during gemini_config.asp consolidation across 118 ASP files. Subfolders use #include virtual=\"/gemini_config.asp\" — works across sub-IIS-apps too.","severity":"critical","tags":"include, const, virtual, file, organization","sortOrder":430},{"id":46,"category":"Database","ruleNumber":"3.13","title":"Per-show activity goes in teamadmin_history; one row per show on the invoice","symptom":"An action (refund, email send, status change) happened on an invoice that covers 3 shows, but only 1 of the 3 shows surfaces the event in its History & Notes timeline.","rootCause":"It's tempting to write a single teamadmin_history row keyed off the invoice. But the activity timeline in TeamAdmin.asp / editshow.asp keys off show_id. One row per show is the only way every show on the invoice surfaces the event.","doPattern":"-- Loop the shows on the invoice and write one row each:\nSet rs = conn.Execute(\"SELECT show_id FROM shows WHERE invoice_id = \" & invoiceID)\nDo Until rs.EOF\n    conn.Execute \"INSERT INTO teamadmin_history \" & _\n        \"(show_id, changed_by_account_id, changed_by_name, change_type, old_value, new_value, notes) \" & _\n        \"VALUES (\" & rs(\"show_id\") & \", \" & Session(\"account_id\") & \", N'\" & _\n        Replace(Session(\"name\") & \"\", \"'\", \"''\") & \"', 'refund_check', \" & _\n        \"'Invoice #\" & invoiceID & \"', '$\" & FormatNumber(refundAmount, 2) & \"', \" & _\n        \"N'Refund check #\" & checkNum & \" to \" & Replace(payee, \"'\", \"''\") & \"')\"\n    rs.MoveNext\nLoop\n-- Also add change_type to the TeamAdmin.asp JS label map so it renders nicely.","dontPattern":"INSERT INTO teamadmin_history (show_id, ...) VALUES (@firstShowId, ...)\n-- the other 2 shows on the invoice never see this event","exampleContext":"BMXRefund.asp LogRefundToShowHistory(). TeamAdmin.asp label map line.","severity":"best-practice","tags":"history, audit, activity, shows, invoice, log","sortOrder":430},{"id":35,"category":"File Organization","ruleNumber":"4.4","title":"Always verify FTP/Dreamweaver upload actually replaced the server file","symptom":"You \"fixed\" the bug locally, uploaded, and the live page still shows the old error. Hours wasted re-editing a file that was already correct on disk because the upload never landed on the server.","rootCause":"Dreamweaver \"Put\" can silently no-op (permissions, locked file, wrong remote path, sub-app webroot, cached connection). The local file is fine but the server still has the old copy. Always compare local-vs-remote before assuming \"my fix is broken.\"","doPattern":"' After upload, ONE of:\n' 1) Dreamweaver ? right-click file ? Compare with Remote ? expect \"files are identical\"\n' 2) View-source the live page and confirm a sentinel comment / new line number is present\n' 3) For VBScript helpers: add a one-line Response.Write \"build_2026_05_29_v3\" and confirm it renders","dontPattern":"' Upload, browser reload, see same error ? assume your code is wrong ? start re-editing ? introduce new bugs","exampleContext":"Hit on compose_email.asp 2026-05 — local file was already clean, server still had the old inline Const. The entire round of debugging was a stale-file issue.","severity":"warning","tags":"deployment, ftp, dreamweaver, file, sync","sortOrder":440},{"id":47,"category":"File Organization","ruleNumber":"4.5","title":"Every ASP file starts with the UTF-8 + CodePage block","symptom":"Curly apostrophes, em-dashes, and accents render as Â or Ã garbage. Page looks fine in the editor, mangled in the browser.","rootCause":"Without an explicit Response.Charset + Response.CodePage, IIS falls back to whatever the server's default is (often Windows-1252). Mixed-encoding content gets double-decoded. The page becomes unreadable for anything beyond ASCII.","doPattern":"<%@ Language=VBScript %>\n<%\nResponse.Buffer      = True\nResponse.ContentType = \"text/html\"\nResponse.Charset     = \"UTF-8\"\nResponse.CodePage    = 65001\n%>\n<!--#include file=\"Dbconn_bn.asp\"-->","dontPattern":"<%@ Language=VBScript %>\n<%\nResponse.Buffer = True\n%>\n<!--#include file=\"Dbconn_bn.asp\"-->","exampleContext":"ManageCodingRules.asp top-of-file. Standard template across BN + BMX.","severity":"warning","tags":"asp, encoding, utf-8, charset, codepage","sortOrder":450},{"id":48,"category":"File Organization","ruleNumber":"4.6","title":"Gate new-table reads behind an On Error check that points at the migration file","symptom":"Page 500s with \"Invalid object name 'BN_CodingRules'\" because the SQL migration was never run on this environment. There's no hint in the error about how to fix it.","rootCause":"Shipping a new feature means shipping both the .asp and the .sql migration. If the .asp is uploaded first, every visitor 500s — and the error doesn't say \"run the migration.\" Always wrap the first query against a new table with On Error Resume Next and surface a friendly migration-pointer message.","doPattern":"On Error Resume Next\nSet rsRules = conn.Execute(\"SELECT * FROM BN_CodingRules ORDER BY Category, SortOrder\")\nIf Err.Number <> 0 Then\n    Response.Write \"<p style=\"\"color:#b81414;padding:16px;background:#fde2e2;border-radius:8px;\"\">\" & _\n        \"Table BN_CodingRules not found. Run the migration first: \" & _\n        \"<code>_migrations/2026-04-29_coding_rules.sql</code></p>\"\n    Err.Clear\nEnd If\nOn Error GoTo 0","dontPattern":"Set rsRules = conn.Execute(\"SELECT * FROM BN_CodingRules\")\n' No fallback if the table doesn't exist yet","exampleContext":"ManageCodingRules.asp line 36-43. Standard pattern for any new-feature page.","severity":"best-practice","tags":"asp, migration, error-handling, schema, deploy","sortOrder":460},{"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":49,"category":"User Input","ruleNumber":"5.4","title":"Escape every Request value before concatenating into SQL","symptom":"Form submits fail randomly when a user types an apostrophe (\"D'Angelo\", \"won't\"). Worse — the form is a SQL-injection vector if an attacker types a closing quote + their own statement.","rootCause":"Classic ASP doesn't have parameterized queries by default. String-concatenating Request.Form values into SQL is the norm, and a single un-escaped apostrophe terminates the literal and breaks (or hijacks) the statement. The SqlStr() wrapper returns either a quoted N' literal or NULL — never raw text — so callers can't forget the quoting.","doPattern":"Function SqlStr(v)\n    If IsNull(v) Then SqlStr = \"NULL\" : Exit Function\n    SqlStr = \"N'\" & Replace(v & \"\", \"'\", \"''\") & \"'\"\nEnd Function\n\nsql = \"INSERT INTO BN_CodingRules (Title) VALUES (\" & SqlStr(Request.Form(\"title\")) & \")\"\nconn.Execute sql","dontPattern":"sql = \"INSERT INTO BN_CodingRules (Title) VALUES ('\" & Request.Form(\"title\") & \"')\"\nconn.Execute sql","exampleContext":"SaveCodingRule.asp + every BN form handler. Use SqlStr() everywhere. Complements rule 3.3 (SqlSafe).","severity":"critical","tags":"sql-injection, security, escape, apostrophe, request-form","sortOrder":540},{"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":36,"category":"LLM APIs","ruleNumber":"6.4","title":"For JSON output from Gemini, set responseMimeType:\"application/json\" — do not just hope","symptom":"Console error: \"Could not parse AI response\" / \"Unexpected token ` in JSON at position 0\". Gemini wraps the JSON in ```json ... ``` markdown fences, or adds a \"Here is your data:\" preamble, and JSON.parse blows up.","rootCause":"Without an explicit response mime type, Gemini's safety/format layer treats the prompt as free-form text and frequently wraps JSON in fences or prose. The fix is one config field, not regex-stripping fences after the fact.","doPattern":"\"generationConfig\": {\n    \"temperature\": 0.4,\n    \"maxOutputTokens\": 8192,\n    \"responseMimeType\": \"application/json\"\n}\n' Gemini will not add fences or preamble — JSON.parse just works.\n' Belt-and-suspenders: still keep a fallback that extracts the first {...} block.","dontPattern":"\"generationConfig\": { \"temperature\": 0.4, \"maxOutputTokens\": 8192 }\n' Then JSON.parse(data.candidates[0].content.parts[0].text) ? SyntaxError on markdown fences","exampleContext":"Hit on keywordPhrases.asp 2026-05 — Gemini was wrapping output in ```json fences. responseMimeType fixed it; regex fallback stays as defense in depth.","severity":"warning","tags":"llm, gemini, json, responsemimetype, parse","sortOrder":640},{"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":40,"category":"HTML Rendering","ruleNumber":"7.4","title":"Inline form values must be HTMLEncoded — including value=\"\" attributes","symptom":"Form fields render broken (closing quotes early) or empty when the loaded value contains a double-quote, ampersand, or angle bracket. XSS surface if the value came from user input.","rootCause":"Server.HTMLEncode handles element text; HA() (attribute-safe escape) is needed inside value=\"...\", placeholder=\"...\", and other attributes. Easy to forget when round-tripping edit forms.","doPattern":"<input type=\"text\" name=\"title\" value=\"<%= HA(rs(\"Title\") & \"\") %>\">\n' HA escapes & \" < and treats Null as \"\"","dontPattern":"<input type=\"text\" name=\"title\" value=\"<%= rs(\"Title\") %>\">\n' breaks if Title contains \"  &  <  or anything HTML-significant","exampleContext":"Pattern used in ManageCodingRules.asp, EditEvent.asp, edituser.asp. HE for element text, HA for attribute values.","severity":"warning","tags":"html, encoding, attribute, xss, forms","sortOrder":740},{"id":50,"category":"HTML Rendering","ruleNumber":"7.5","title":"Don't scope base text/background rules to #editor only","symptom":"Page looks fine inside the CMS preview/editor, then ships dark/cream/blank on the live site — headlines and body copy go invisible against the section background.","rootCause":"CMS page builders wrap content in #editor while editing but render it inside .page-wrapper (or no wrapper) when live. Any rule scoped only to #editor stops matching the moment the page leaves the editor, so the cascade never delivers your base color or background.","doPattern":"#editor,\n.page-wrapper {\n    color: #fff !important;\n    background: var(--navy-deep) !important;\n    font-family: 'DM Sans', sans-serif;\n}","dontPattern":"#editor { color: #fff; background: var(--navy-deep); font-family: 'DM Sans',sans-serif; }","exampleContext":"rankingmastery/clients-html/frank.html. Same pattern hits every CMS-embedded page.","severity":"critical","tags":"css, cms, page-builder, editor, scope, embed","sortOrder":750},{"id":51,"category":"HTML Rendering","ruleNumber":"7.6","title":"Default state of every element must be visible (no JS-dependent reveals)","symptom":"Entire sections (reviews, cards, panels, banners) are blank on the live page. Inspector shows the text is there with the right color — it's just opacity:0 and never animates in.","rootCause":"The page builder strips inline <script> blocks on save, leaving the CSS resting state (opacity:0; transform:translateY(38px)) with no class-adder to flip it to visible. The \"scroll-reveal\" code becomes /* disabled in editor */ and the content sits invisible forever.","doPattern":".fl-sr {\n    opacity: 1;\n    transform: none;\n    /* If you really want a reveal, use pure CSS @keyframes, never a JS class-adder: */\n    animation: fadeUp .7s ease both;\n}\n@keyframes fadeUp { from { opacity:0; transform:translateY(20px); } to { opacity:1; transform:none; } }","dontPattern":".fl-sr { opacity: 0; transform: translateY(38px); transition: opacity .8s, transform .8s; }\n.fl-sr.v { opacity: 1; transform: translateY(0); }\n<script>/* Scroll animations disabled in editor */</script>","exampleContext":"rankingmastery/clients-html/frank.html — the .fl-sr opacity:0 bug. Same root cause as embed-page-prompt.md rule 2.","severity":"critical","tags":"css, animation, opacity, reveal, javascript, cms","sortOrder":760},{"id":52,"category":"HTML Rendering","ruleNumber":"7.7","title":"Override the CMS .widget-container background, not just the inner form fields","symptom":"A form (or any widget) renders as a bright white panel on top of your dark card. The labels and inputs you styled for a dark theme are invisible against the white.","rootCause":"The CMS injects a parent <div class=\"widget-container\"> with a hardcoded white-gradient inline background that sits on top of whatever card you wrap it in. Styling only the inputs and labels doesn't help — the outer container is what's covering your card.","doPattern":".fl-form-card .widget-container,\n.fl-form-card .widget-container.widget-optin {\n    background: transparent !important;\n    padding: 0 !important;\n    border-radius: 0 !important;\n    box-shadow: none !important;\n}\n.fl-form-card input { background: rgba(255,255,255,0.08); color: #fff; }\n.fl-form-card label { color: rgba(255,255,255,0.85); }","dontPattern":".fl-form-card input { background: rgba(255,255,255,0.08); color: #fff; }\n.fl-form-card label { color: rgba(255,255,255,0.85); }","exampleContext":"rankingmastery/clients-html/frank.html form-widget fix. Same shape as the embed-page-prompt rule 5.","severity":"warning","tags":"css, cms, widget, form, override, important","sortOrder":770},{"id":53,"category":"HTML Rendering","ruleNumber":"7.8","title":"Never paste the page-wrapper + fonts <link> block more than once","symptom":"Page renders fine but Inspector shows 4–6 nested <div class=\"page-wrapper\"> elements with duplicated Google Fonts <link> tags. Hidden cost: pages weigh 3-5× what they should and layout math (vh, %, flex) misbehaves.","rootCause":"Copy-pasting the \"starter template\" into a CMS that already injects the same boilerplate doubles up the wrapper. Each subsequent paste nests deeper. The CMS doesn't complain — fonts still load, divs still render — but everything is fragile.","doPattern":"<!-- Just the inner content. The CMS injects .page-wrapper for you. -->\n<style> /* your scoped styles */ </style>\n<section class=\"hero\">...</section>\n<section class=\"features\">...</section>","dontPattern":"<div class=\"page-wrapper\" style=\"min-height:100vh;background:#f5f5f7;\">\n  <div class=\"page-wrapper\" style=\"min-height:100vh;background:#f5f5f7;\">\n    <div class=\"page-wrapper\" style=\"...\">\n      <!-- actual content way down here -->\n    </div>\n  </div>\n</div>","exampleContext":"RankingMastery SEO page generator. Strip nested wrappers in the renderer before writing the file.","severity":"warning","tags":"html, cms, nesting, duplication, wrapper","sortOrder":780},{"id":54,"category":"HTML Rendering","ruleNumber":"7.9","title":"Don't rely solely on a Font Awesome kit URL — add a public CDN fallback","symptom":"Every <i class=\"fa-solid fa-...\"> renders as a blank circle/square on the live site, even though the same page worked yesterday. DevTools Network tab shows the kit script returning 401/403.","rootCause":"Font Awesome kits (kit.fontawesome.com/XYZ.js) are domain-locked to the URLs whitelisted in your Font Awesome account. The moment the page renders on a new subdomain or a staging URL, the kit silently fails — IIS doesn't even log it because the failure is in the browser.","doPattern":"<head>\n    <!-- Kit (preferred when allowed) -->\n    <script src=\"https://kit.fontawesome.com/39079c2f95.js\" crossorigin=\"anonymous\"></script>\n    <!-- Public CDN fallback so icons still render on every domain -->\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css\">\n</head>","dontPattern":"<head>\n    <script src=\"https://kit.fontawesome.com/39079c2f95.js\" crossorigin=\"anonymous\"></script>\n</head>","exampleContext":"rankingmastery/menu.asp — kit silently 401'd on a new subdomain. Both BMX and FL hit this.","severity":"warning","tags":"fontawesome, icons, cdn, kit, domain-lock","sortOrder":790},{"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":55,"category":"Cookies & Auth","ruleNumber":"8.2","title":"Admin pages start with an explicit Session(\"account_id\") check that 302s on fail","symptom":"A guest hits /BusinessNetworking/ManageCodingRules.asp directly and gets the full admin form. Or a logged-in non-admin can view another tenant's data because the page only filters by Session(\"account_id\") in the WHERE clause but never verifies it's the right user type.","rootCause":"It's easy to assume the menu link is the auth — but URLs are guessable. Without an explicit top-of-file gate, anyone with the URL can hit admin endpoints. And a missing account_id check means the SQL runs as whatever Session has (often empty), exposing default-tenant data.","doPattern":"<%\nDim adminID : adminID = Session(\"account_id\")\nIf adminID = \"\" Or Not IsNumeric(adminID) Then\n    Response.Redirect \"/BusinessNetworking/login.asp?return=\" & Server.URLEncode(Request.ServerVariables(\"URL\"))\n    Response.End\nEnd If\n' Optional second gate for super-admin only:\nIf CStr(adminID) <> \"8748\" Then\n    Response.Status = \"403 Forbidden\"\n    Response.Write \"Not authorized.\"\n    Response.End\nEnd If\n%>","dontPattern":"<%\n' (no auth check, page just runs)\nSet rs = conn.Execute(\"SELECT * FROM BN_CodingRules\")\n%>","exampleContext":"ManageCodingRules.asp, menu-admin.asp, every Manage*.asp page.","severity":"critical","tags":"auth, session, admin, security, redirect","sortOrder":820},{"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},{"id":37,"category":"Error Handling","ruleNumber":"9.2","title":"CDO.Message — set HTMLBody before touching HTMLBodyPart.Charset","symptom":"Runtime error \"Object required: 'HTMLBodyPart'\" when sending email, or the email sends as garbled UTF-8 with no charset set.","rootCause":"CDO's HTMLBodyPart is a derived COM object — it does NOT exist until you assign HTMLBody. Setting .Charset on it before HTMLBody is like dereferencing a null pointer. The endpoint then returns \"bad_response\" because the function bails before writing JSON.","doPattern":"Set msg = CreateObject(\"CDO.Message\")\nmsg.HTMLBody = htmlBody                 ' assign FIRST\nmsg.HTMLBodyPart.Charset = \"utf-8\"      ' now safe — body part exists\nmsg.BodyPart.Charset    = \"utf-8\"       ' optional: plain-text part charset too","dontPattern":"Set msg = CreateObject(\"CDO.Message\")\nmsg.HTMLBodyPart.Charset = \"utf-8\"   ' Object required — HTMLBodyPart doesn't exist yet\nmsg.HTMLBody = htmlBody","exampleContext":"Hit in cdoSend in request_submit.asp on 2026-05. The wrong order swallowed the email send, and an \"On Error GoTo 0\" further down meant the bubble-up turned into a 500 with a 1-byte response body.","severity":"critical","tags":"cdo, email, smtp, charset, order","sortOrder":920},{"id":38,"category":"Error Handling","ruleNumber":"9.3","title":"Call Response.Clear before writing JSON error output","symptom":"Client-side fetch reports \"bad_response\" / \"Unexpected token < in JSON\". Your endpoint did write valid JSON, but a stray <%= ... %> or include leaked HTML into the response buffer earlier and the JSON parser chokes on it.","rootCause":"Response.Buffer=True keeps all writes pending until flush. Any upstream stray output (include side effects, ASP error pages, a #include that emits whitespace) lives in the buffer. emitError adds JSON on top, so the client sees HTML-then-JSON.","doPattern":"Sub emitError(msg)\n    Response.Clear                  ' drop anything upstream buffered\n    Response.ContentType = \"application/json\"\n    Response.Charset = \"UTF-8\"\n    Response.Write \"{\"\"ok\"\":false,\"\"error\"\":\"\"\" & JSONEsc(msg) & \"\"\"}\"\n    Response.End\nEnd Sub","dontPattern":"Sub emitError(msg)\n    Response.ContentType = \"application/json\"\n    Response.Write \"{\"\"ok\"\":false,\"\"error\"\":\"\"\" & JSONEsc(msg) & \"\"\"}\"\n    Response.End\nEnd Sub\n' Any buffered HTML/whitespace ends up prefixing the JSON","exampleContext":"Hit on request_submit.asp 2026-05. Added Response.Clear so the client always sees a clean JSON body even when something upstream emitted HTML.","severity":"warning","tags":"response, buffer, json, error, api","sortOrder":930},{"id":39,"category":"Error Handling","ruleNumber":"9.4","title":"Keep On Error Resume Next scope tight — never re-enable strict errors mid-Sub","symptom":"A Sub that \"had OERN at the top\" still throws an unhandled 500 deep inside. Adding \"On Error GoTo 0\" mid-function unexpectedly re-enables strict error trapping for everything after that line, including CDO/COM/SQL calls that assumed OERN was still active.","rootCause":"OERN is a procedure-level switch, but \"On Error GoTo 0\" turns it OFF anywhere it appears — including in the middle of the same Sub. Code added later (CDO send, recordset open, JSON build) that assumes \"we're in OERN\" will then crash hard.","doPattern":"Sub cdoSend(...)\n    On Error Resume Next\n    ' ... entire Sub ...\n    msg.Send\n    If Err.Number <> 0 Then\n        errDesc = \"CDO send failed: \" & Err.Description\n        Err.Clear\n    End If\n    ' On Error GoTo 0 is at the CALLER, not inside the Sub\nEnd Sub","dontPattern":"Sub cdoSend(...)\n    On Error Resume Next\n    ' ... CDO setup ...\n    On Error GoTo 0          ' OERN now OFF for rest of Sub\n    msg.Send                 ' any CDO error here is now an unhandled 500\nEnd Sub","exampleContext":"Hit in cdoSend / request_submit.asp 2026-05. The mid-function On Error GoTo 0 turned a recoverable CDO failure into a hard 500 + bad_response on the client.","severity":"warning","tags":"error, oern, on-error, scope, vbscript","sortOrder":940}]}