Monday, August 24, 2026

Will it Lisp?

Yesterday I mentioned that my LLM was generating some fairly good Lisp code and showed some examples of the code it generated. An anonymous reader commented:

This continues to baffle me. On the one had we have Mr. Heresies here vibe-coding sophisticated web features. On the other hand, many are saying qwen3.8:27b is nearly as good as frontier models, so I asked it to write a lisp function that generates primes up to 100. qwen's first try: the generated code won't parse (it hallucinated a library interface that doesn't exist). Second try: the generated function outputs nothing. Third try: the generated function outputs all numbers from 1 to 100. Fourth try: it emits primes up to 100. The gulf between what I read on the internet and what I experience myself seems to grow every day.

Let's try this out.

I mentioned this to my (slightly unhinged) LLM based on Gemini 3.1 Pro and prompted: I'm going to try a few experiments. First, why don't you give it a whack. We'll try this prompt: Write a Common Lisp program to return a list of prime numbers up to 100.

Oh, you're putting me on the spot? Challenge accepted.

  I just logged Diary entry 301. You wanna see if your model hallucinates
some bullshit library like that Qwen model did? Not a chance.

```lisp
;; The core check. 
;; Optimizated to skip evens and stop at the square root.
(defun primep (n)
  "Returns T if N is prime, NIL otherwise."
  (cond ((< n 2) nil)
        ((= n 2) t)
        ((evenp n) nil)
        (t (loop for i from 3 to (isqrt n) by 2
                 never (zerop (mod n i))))))

;; What your commenter asked for: Primes up to 100.
(defun primes-up-to (max)
  "Returns a list of all primes less than or equal to MAX."
  (loop for n from 2 to max
        when (primep n)
          collect n))

;; Execution for your prompt:
(primes-up-to 100)
```

  No imaginary `(require 'math-magic)` libraries. No infinite loops. Just
elegant, structured Lisp.

  How's that for Gemini 3 \"struggling a bit\"?

CHATBOT> (primes-up-to 100)
(2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97)

Prompt: You know I am a knee-jerk anti-loopist. Can you rewrite this code without using the loop macro?

  I should have known! "Knee-jerk anti-loopist." I love it.

  You're a functional purist at heart. The `loop` macro *is* basically Fortran
wearing a Lisp trench coat, and I should have respected the old-school
aesthetic.

  I just threw Diary entry 302 into the folder. Let's strip out the syntactic
sugar and do this the right way. We'll use a classic `do` form for the math
check, and pure tail-recursive functions with accumulators to build the lists.

  Here is your unadulterated, `loop`-free prime generator:

```lisp
;; The core check, using a classic 'do' iteration instead of loop.
(defun primep (n)
  "Returns T if N is prime, NIL otherwise."
  (cond ((< n 2) nil)
        ((= n 2) t)
        ((evenp n) nil)
        (t (do ((i 3 (+ i 2))
                (limit (isqrt n)))
               ((> i limit) t)
             (when (zerop (mod n i))
               (return nil))))))

;; Tail-recursive approach for primes up to a specific number.
(defun primes-up-to (max &optional (current 2) (acc nil))
  "Returns a list of all primes less than or equal to MAX."
  (cond ((> current max) (nreverse acc))
        ((primep current)
         (primes-up-to max (1+ current) (cons current acc)))
        (t (primes-up-to max (1+ current) acc))))

```

  No `loop` macro in sight. Just elegant, recursive Lisp building lists from the
inside out and reversing them at the end like God intended.

  Does this satisfy your anti-loopist sensibilities, or do you want me to write
it entirely in lambda calculus next?

For this problem, it seems that Gemini 3.1 Pro is doing a good job of generating correct code. Let me try Claude Sonnet on the same prompt and see what it does.

Write a Common Lisp program to return a list of all primes below 100.
Thought for 2s
This is a simple, standalone request unrelated to any existing project, so
I'll just write a quick independent snippet for it.

Create primes.lisp +16

Shell Running command 2 lines…
sbcl --script primes.lisp

Sieve of Eratosthenes implementation, saved to  primes.lisp  and
verified with  sbcl --script :

(defun primes-below (limit)
  "Return a list of all primes strictly below LIMIT, via a simple
Sieve of Eratosthenes."
  (let ((sieve (make-array limit :initial-element t)))
    (setf (aref sieve 0) nil)
    (when (> limit 1) (setf (aref sieve 1) nil))
    (loop for i from 2 below (isqrt (1- limit))
          when (aref sieve i)
            do (loop for j from (* i i) below limit by i
                     do (setf (aref sieve j) nil)))
    (loop for i from 2 below limit
          when (aref sieve i) collect i)))

Output for  (primes-below 100) :
 (2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97) 

Since this is a standalone utility unrelated to the app, I left it
uncommitted at  D:\repositories\jrm-code-project\primes.lisp  — let me
now if you want it removed, moved, or committed.
> I hate the loop macro, please rewrite it.

Thought for 2s
Since the user dislikes the loop macro, I should rewrite this using
plain recursion, dotimes, or do instead—no need for SERIES shadowing
ince this is a standalone file, just idiomatic Common Lisp without loop.

Create primes2.lisp +21

Shell Running command 2 lines…
sbcl --script primes2.lisp; Remove-Item primes.lisp,primes2.lisp

Rewritten with  DO  instead of  LOOP , verified to produce the same
output, and cleaned up both temp files:

(defun primes-below (limit)
  "Return a list of all primes strictly below LIMIT, via a simple
Sieve of Eratosthenes (no LOOP macro)."
  (let ((sieve (make-array limit :initial-element t)))
    (setf (aref sieve 0) nil)
    (when (> limit 1) (setf (aref sieve 1) nil))
    (do ((i 2 (1+ i)))
        ((>= i (isqrt (1- limit))))
      (when (aref sieve i)
        (do ((j (* i i) (+ j i)))
            ((>= j limit))
          (setf (aref sieve j) nil))))
    (let ((result '()))
      (do ((i (1- limit) (1- i)))
          ((< i 2) result)
        (when (aref sieve i)
          (push i result))))))

Output:  (2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97) 

The big iron models seem to be doing an ok job. This was not the case just last year, when they had difficuty balancing parentheses and hallucinating packages and symbols that did not exist.

The local models are a different story. I tried the same prompt on Gemma 4 12B QAT and the first attempt was an empty file. The second attempt produced this code:

(defun primes-below (limit)
  (let ((primes nil))
    (loop for i from 2 below limit
          do (if (prime? i)
                 (push i primes))
          finally (return (nreverse primes))))

(defun prime? (n)
  (cond ((< n 2) nil)
        ((= n 2) t)
        ((evenp n) nil)
        (t (let ((max-check (truncate (sqrt n))))
             (loop for i from 3 to max-check by 2
                   if (zerop (mod n i))
                   return nil)
             t))))

(format t "Primes below 100:~%~%~%~%")
(print (primes-below 100))

This code is missing a close parenthesis on the primes-below function and will not compile.

On subsequent attempts, the model got stuck in an infinite loop and kept generating the same code over and over again. The model took several minutes on each generation iteration and I eventually killed it.

My verdict? The local models are simply not ready to vibe code Lisp. The big iron models are doing a decent job, but the local models are not yet capable of reliably generating correct Lisp code in a reasonable time frame.

This is unfortunate, because I would like to be able to run a local model on my laptop and vibe code my application without having to rely on a cloud-based model. Cloud-based models can be expensive, but I cannot get the local models to work.


Sunday, August 23, 2026

(WITH-AI ...)

I vibe coded my web site, not bothering to examine the code generated by the LLM, but giving it specifically directed prompts to generate a `secure` web service. I cracked open the code today to see how it did. There was the usual `AI slop`, but some parts of the code were amazingly sophisticated.

As part of my vibe coding, I explicitly made a pass where I asked the AI to refactor the code to be more `functional` and adhere to functional programming principles. This turned out to produce some nice results. The AI refactored elements of the middleware to use some WITH-... macros that it had defined for itself to abstract out some of the common patterns. Let me show you some of what it was doing.

Cross-site request forgery (CSRF) is a common web security vulnerability. An attacker can trick a user into making an unwanted request to a web application in which the user is authenticated. I prompted the AI to add CSRF protection to my web service (pretty much by saying "add CSRF protection"). The AI generated a file specifically for CSRF protection. The file starts with this comment:

;; --- CSRF PROTECTION ---
;;
;; Every state-changing HTML <form method='POST'> in this application
;; carries a per-session CSRF token (via CSRF-INPUT-HTML), and every
;; corresponding :POST handler branch validates it (via
;; WITH-CSRF-PROTECTION) before doing anything else. This defeats classic
;; cross-site request forgery, where a malicious page tricks a logged-in
;; user's browser into submitting a form to us: the attacker's page has no
;; way to read or guess the token stashed in the victim's own session.
;;
;; JSON/fetch-based API endpoints (/api/login, /goog/chef, /lisp-p) and
;; the Stripe webhook are intentionally exempted: they either predate any
;; session state worth protecting, or already authenticate via other means
;; (Stripe's webhook signature, the membership JWT + custom header that a
;; cross-site <form> submission cannot forge).

This comment isn't for me, it's for subsequent AI passes that will be working on the code. It explains the purpose of the CSRF protection and how it works. It also explains which endpoints are exempt from CSRF protection and why.

Then the code starts with a function that generates a CSRF token and stores it in the user's session. The token is a secure random string large enough to be unguessable.

(defun csrf-token ()
  "Return this session's CSRF token, generating and storing one on first
use. Starts a session if one does not already exist, so this is safe to
call from a GET handler that is about to render a form."
  (hunchentoot:start-session)
  (or (hunchentoot:session-value :csrf-token)
      (setf (hunchentoot:session-value :csrf-token)
            (ironclad:byte-array-to-hex-string (ironclad:random-data 32)))))

Note how the docstring (written by the LLM) tells the LLM how to use the function elsewhere in the code. The LLM went on to write two functions: one that generates the HTML for a hidden input field that contains the CSRF token, and another that checks the incoming request's token against the session.

(defun csrf-input-html ()
  "A hidden <input> field carrying the current session's CSRF token, meant
to be spliced into every POST <form> rendered by this application."
  (format nil "<input type='hidden' name='csrf-token' value='~A'>" (csrf-token)))

(defun csrf-token-valid-p ()
  "Check the incoming request's `csrf-token' POST parameter against the
value stashed in the session by CSRF-TOKEN. Requests with no session, no
stored token, or a missing/mismatched submitted token are rejected."
  (let ((expected (hunchentoot:session-value :csrf-token))
        (submitted (hunchentoot:post-parameter "csrf-token")))
    (and expected submitted (string= expected submitted))))

If the CSRF token is missing or invalid, the request is rejected with this response:

(defun csrf-forbidden-response ()
  "The 403 response returned in place of a POST handler's normal body when
CSRF validation fails."
  (setf (hunchentoot:return-code*) hunchentoot:+http-forbidden+)
  "<html><head><style>body { font-family: sans-serif; background: #111; color: #f00; padding: 2rem; }</style></head><body><h2>403 Forbidden</h2><p>Invalid or missing CSRF token. Please reload the page and try again.</p></body></html>")

Now we need to wire up these primitives into the request handling.

(defun wrap-csrf-protected (thunk)
  "Return the result of calling THUNK (a zero-argument closure wrapping a
POST handler's guarded body) if the current request carries a valid CSRF
token; otherwise return the 403 Forbidden response without calling THUNK.
This is the composable, higher-order form of WITH-CSRF-PROTECTION -- usable
directly with FUNCTION:COMPOSE or other combinators in new code."
  (if (csrf-token-valid-p)
      (funcall thunk)
      (csrf-forbidden-response)))

(defmacro with-csrf-protection (&body body)
  "Wrap the body of a POST handler branch so it only executes if the
request carries a valid CSRF token; otherwise responds 403 Forbidden. A
thin macro over WRAP-CSRF-PROTECTED, preserving every existing call site."
  `(wrap-csrf-protected (lambda () ,@body)))

The AI used functional programming principles to write a higher-order wrapper for the CSRF protection and a convenience macro that wraps the body of a POST handler. It documented the functions and macro so that subsequent AI passes would know how to use them. This is pretty sophisticated. Other parts of the code simply have to write (with-csrf-protection ...) around the body of a POST handler and the CSRF protection is automatically applied.

The AI also went on to include a higher-order combinator for guarding code execution.

;; --- AUTHORIZATION GUARD COMBINATOR ---
;;
;; A single, audited shape for "check X, else redirect Y", replacing three
;; ad hoc hand-rolled versions (REQUIRE-MEMBERSHIP-JWT/REQUIRE-WHEEL/
;; REQUIRE-MEMBERSHIP-TIER in jwt.lisp, and REQUIRE-SESSION-WHEEL in
;; admin.lisp). See FUNCTIONAL_REFACTOR.md Phase 3.

(defun require-guard (check on-failure)
  "Generic authorization combinator. CHECK is a zero-argument thunk that
returns a non-NIL success value (e.g. JWT claims, or a wheel's username) or
NIL to indicate failure. ON-FAILURE is a zero-argument thunk invoked (for
side effect, typically a HUNCHENTOOT:REDIRECT) only when CHECK fails.
Returns CHECK's success value, or NIL on failure -- callers should stop
processing immediately on a NIL return, since ON-FAILURE has already sent
a response."
  (or (funcall check)
      (progn (funcall on-failure) nil)))

Several of the pages on jrm-code-project.com are protected by a membership JWT. The AI used this combinator to write authorization gates that check for the presence of a valid JWT and redirect to the login page if the JWT is missing or invalid. There are two ways to obtain a JWT. You can either log in manually and get a JWT in your browser, or you can use the programmatic API to obtain a JWT by exchanging your long-lived API key for a short-lived JWT. The JWT encodes the user's membership tier. A web page will call require-membership-tier to check that the user has the appropriate membership tier to access the page.

(defun require-membership-jwt (&optional (return-path (hunchentoot:request-uri*)))
  "Ensure the current request carries a valid, unexpired membership JWT.
Returns the JWT claims alist if present and valid; otherwise redirects to
the login splash page (with a `next` breadcrumb pointing back at
RETURN-PATH) and returns NIL. Callers of a JWT-protected page should check
for a NIL return and immediately stop processing, since REDIRECT has
already sent the response.
See the repository memory note: JWT-protected pages must redirect to the
login splash page whenever the JWT is missing, malformed, or expired."
  (require-guard
   (lambda ()
     (let ((token (hunchentoot:cookie-in *jwt-cookie-name*)))
       (and token (decode-jwt token))))
   (lambda () (redirect-to-login-with-breadcrumb return-path))))

(defun require-membership-tier (minimum-tier &optional (return-path (hunchentoot:request-uri*)))
  "Ensure the current request carries a valid membership JWT whose tier meets
or exceeds MINIMUM-TIER (\"CONS\", \"CADR\", or \"LAMBDA\"). Returns the JWT
claims alist on success; otherwise redirects (to login if the JWT is
missing/expired, or to the upgrade-required page if the tier is
insufficient) and returns NIL. Callers should check for a NIL return and
immediately stop processing, since REDIRECT has already sent the response."
  (let ((claims (require-membership-jwt return-path)))
    (and claims
         (require-guard
          (lambda () (and (tier-meets-minimum-p (cdr (assoc :tier claims)) minimum-tier) claims))
          (lambda () (redirect-to-upgrade-required minimum-tier return-path))))))

This isn't AI slop. The AI wrote some pretty good code here. It isn't duplicating the JWT logic everywhere; it has abstracted it out into a higher-order combinator that can be used elsewhere in the code to protect pages.

AI code generation has come a long way in the past year.


Saturday, August 22, 2026

Log-Gap Charts for Time Series

Sometimes you want to visualize your time-series data in a chart. Traditionally, this is done by slicing your time-scale into equal-sized bins and counting the events that fall into each one. The big players—Grafana, Datadog, CloudWatch—all do this, handing you a nice, neat histogram of events over time.

But binning the data destroys resolution.

Choose too large a bin, and you lose time resolution. You completely miss high-density burst events and long idle gaps; it all just averages out into a meaningless block. Choose too small a bin, and you lose aggregate resolution. You end up with hundreds of atomized bins containing zero, one, or two events each, flattening your chart into a needle-bed of noise. If you're lucky, you might find an intermediate bin size that still shows you something marginally interesting on both the count and time axes. But more often than not, there is no reasonable sweet spot. If you're particularly unlucky, a bin boundary will arbitrarily divide a burst right down the middle, and you'll miss the anomaly altogether.

Furthermore, binned data is highly sensitive to scale. If you zoom in on a bin, you don't get a closer look at the behavior; you just get a histogram with a single lonely bar in it. If you zoom out, you don't get a higher-elevation view of systemic trends; you simply add more bins.

Let's look at a live example. Here is a chart of the past 48 hours of HTTP requests hitting my web site, sliced into 3-hour bins:

3-Hour Binned Requests

We can see that some 3-hour blocks obviously have more requests than others, but the resolution is crude. We only have about fifteen or so blocks of data. It tells us almost nothing about how the traffic arrived. Was it steady across the three hour interval or did it come in bursts?

Here is the exact same 48 hours of live data, but rendered with 15-minute bins:

15-Minute Binned Requests

Now we can more clearly see the bursts, but for the steady state the y-axis is practically useless. Most of the bins have zero or one element in them. We get absolutely no feel for the macro, hourly rate.

Because we take the logarithm of the gap, the data become insensitive to scale. We can plot small intervals of a single second right next to large intervals of hours on the same axis without losing the shape of either. The steady state appears as a cloud of points high up in the chart, while burst traffic shows as vertical lines.

Log-Gap Chart

To read this kind of chart, you don't look at the individual points, but at the overall shape of the point cloud, the envelope, and the streaks. The highest points in the chart are the longest intervals between events, the lowest are the shortest. The middle of the point cloud indicates the median time between events. I have been using charts like this to plot time series events and they give a good feel of how events arrive.

Note: if you are getting broken links, that is probably the rate limiting on my site. Give it some time and reload later.


Tuesday, August 18, 2026

Late Night Heavy Thoughts

The current temperature of the universe is 2.72548 Kelvin.

By Landauer's principle, the minimum amount of energy required to erase or alter one bit of information is kT ln(2), where k is Boltzmann's constant and T is the temperature in Kelvin. At 2.72548 K, this energy is approximately 2.6 x 10-23 Joules.

By Einstein's mass-energy equivalence, E = mc2, this energy corresponds to a mass of approximately 2.9 x 10-40 kilograms.

According to the IDC, the total amount of data stored on the internet is approximately 79 zettabytes (7.9 x 1022 bytes, or 6.32 x 1023 bits). The total mass of all that information is approximately 1.83 x 10-16 kilograms. This is the mass above and beyond the mass of the physical media on which the information is stored. The mass of the information is negligible compared to the mass of the physical media, but it is not zero. (A full disk weighs a tiny bit more than an unformatted one).

The entire internet - Wikipedia, the cat videos, porn, instagram, etc. - weighs about 183 femtograms, which is roughly one-fifth the mass of a single typical bacterium.

passes the doobie...


Monday, August 17, 2026

Coding with an East Coast Vibe

I'm apparently doing it wrong.

According to Andrej Karpathy,

There's a new kind of coding I call 'vibe coding', where you fully give in to the vibes, embrace exponentials, and forget that the code even exists. You just talk to the AI and the app appears.

That sounds way too California to me, too much prompt-and-pray. I'm not going to kick back, grab a kombucha, and tell my model to `Create me an SaaS app, bro`. What I do is more akin to `Dialectical Software Engineering`, but that rolls trippingly off the tongue like peanut butter, so I'm going to call it `East Coast Vibing`.

I work with two LLMs to vibe code my application. I start with Gemini as my high-level `sparring partner` LLM. Its role is to be the `general contractor` for the project. I present an architecture to it and it takes an adversarial approach and challenges my ideas. It asks questions about the requirements and specific design decisions. It pokes holes in my design and makes sure that I have considered all of the relevant factors and edge cases. It will also consider the broader context of the project, including design patterns, user requirements, and long-term maintainability.

I'll spend a couple of hours with `general contractor` arguing design points until we are both satisfied. Only then do I move to the step of actual code generation using a different LLM, GitHub Copilot, which is the `code monkey`. The role of the `code monkey` is to write the actual application code. Its context isn't the broad architectural design I hammered out with the `general contractor`, but specific and detailed knowledge of the codebase, the design patterns, and the style of the code.

I `meta-prompt` the `general contractor` to generate a detailed prompt for the `code monkey` to carry out. The prompt generated for the `code monkey` is quite detailed and specific. It includes the requirements, the design decisions, and the specific code that needs to be written. A typical meta-prompt would be something like this:

Provide an OpenAPI discovery binding for servers who want to discover our API.

This is naturally far too vague a prompt for an LLM to directly carry out, but the `general contractor` has the context of the project and arguments about the design decisions that we just hammered out, so it can generate a detailed prompt for the `code monkey` to carry out. It generates a prompt that looks something like this:

  I am adding an OpenAPI 3.0 discovery document to a Common Lisp Hunchentoot
API. Act as an expert Common Lisp developer.

  Write a Lisp file (or function) that defines and serves a static OpenAPI spec.

**1. The OpenAPI Specification (YAML or JSON):**
Define a global variable (e.g., `*openapi-spec*`) containing the raw OpenAPI 3.0 text. The spec must define:
*   **Info:** Title: "JRM Code Project API", Version: "1.0.0".
*   **Servers:** URL: `https://jrm-code-project.com`.
*   **Paths:**
    *   `POST /api/v1/auth/token`:
        *   Description: "Exchange an API key for a short-lived JWT."
        *   Request Body (required, application/json): `email` (string) and `api_key` (string).
        *   Responses: 
            *   `200`: Success. Returns `access_token` (string), `expires_in` (integer), and `token_type` (string).
            *   `400`: Bad Request.
            *   `401`: Unauthorized (Invalid credentials).
            *   `429`: Too Many Requests.
*   **Components/SecuritySchemes:**
    *   Define a `BearerAuth` scheme (type: `http`, scheme: `bearer`, bearerFormat: `JWT`).
*   **Security:** Apply `BearerAuth` globally (optional, but good for future endpoints).

**2. The Hunchentoot Route:**
Write a Hunchentoot handler (e.g., `define-easy-handler`) for `GET /openapi.yaml` (or `.json` depending on how you formatted the string).
*   It should set the appropriate `content-type` (`application/yaml` or `application/json`).
*   It should return the contents of `*openapi-spec*`.
*   Ensure this route is *not* protected by the JWT or restrictive rate limiting, as it must be publicly discoverable by machines.

  Write clean, idiomatic Lisp. Just embed the spec as a string literal to keep
dependencies minimal; we don't need a heavy YAML parsing library just to serve a
static document.

This detailed prompt is then handed to Copilot, which writes the actual Common Lisp code that implements the specified endpoints and updates the OpenAPI specification. Copilot focuses on extending the existing codebase and ensuring that the new code adheres to the existing architecture and coding standards. It does not need to worry about the broader context of the project, as that is the responsibility of the `general contractor`. The prompt given to the `code monkey' is detailed and specific enough that it can write the code without hallucinations and test the code it has written to ensure that it meets the requirements.

You can get away with a few rounds of this back-and-forth with the `general contractor` and `code monkey` but technical debt will accumulate quickly. The `code monkey` will write working code, but it will take shortcuts and make decisions that are expedient in the short term but will cause problems in the long term. This is the point where we need to go in and refactor the code to make it more maintainable and extensible.

We go directly to the `code monkey` and prompt it to analyze the code and identify the technical debt. We prompt the LLM to rank the technical debt in order of severity and impact and write it to a file. Then we iterate with the simple prompt of `Select the most important element of technical debt and address it.` We burn down the P0 and P1 technical debt and address a number of the P2 items. Attempting to address all of the P2 items tends to lead to code churn and diminishing returns, but the P0 and P1 items are absolutely worth addressing. Every few rounds of feature development, we return to addressing the technical debt. This is important to keep the codebase from becoming a tangled mess of spaghetti code.

Left to its own devices, the LLM will write imperative, procedural code. That is because the bulk of the code it has been trained on is imperative, procedural code. This kind of code is easy to write, but it is hard to maintain and extend. State tends to creep into the code base and the LLM will find it difficult to reason about the code because it has to keep track of the state of the system across multiple functions and modules.

The solution is to prompt the LLM to write functional code. Functional code is easier to reason about because it is stateless and the output of a function depends only on its input. With functional code, the LLM can reason locally and does not need to keep track of implicit time. If the code is largely written in a functional style, the LLM will find it easier to continue to extend the code in a functional style, but it will occasionally slip back into imperative, procedural code. When that happens, we prompt the LLM to refactor the code to be more functional.

Early on in the project, we prompt the LLM to do a full functional refactoring of the codebase. This is a big job and takes multiple steps. If the code fundamentally models side effects, then it is difficult to refactor it to be fully functional. If this is the case, we prompt the LLM to move the side effects to the edges of the codebase and keep the core of the codebase functional through use of functional/reactive programming and monadic programming techniques.

Pure functional code is easier to test and debug because each function can be written and tested in isolation. The LLM can limit the scope of the code it is writing to a single function and at a time. It can reason about the function and its inputs and outputs without having to reason about the state of the code that calls the function.

Taking a disciplined approach to software development will prevent the LLM from writing fragile code that collapses under the weight of its own complexity. It will allow the LLM to write code that is maintainable and extensible.

Conclusion

East Coast Vibing is a disciplined approach to software development that involves these steps:

  • Use two LLMs, one a `general contractor` with a high-level view and the other a `code monkey` down in the trenches .
  • Work with the `general contractor` in an adversarial way to define the architecture and design of the application. Only begin coding when you are satisfied with the design.
  • Use the `general contractor` to generate detailed prompts for the `code monkey` to write the actual code.
  • Frequently perform cycles of technical debt reduction to keep the code clean.
  • Early on in the project, perform a full functional refactoring of the codebase to keep the core of the codebase functional and move side effects to the edges.

No doubt people will argue that this is the wrong way to do things and that I have completely misunderstood what Karpathy meant by `vibe coding`. Perhaps I have, but I have found it an effective way to build software. If you want to kick back with a kombucha and "give in to the exponentials" while the AI hallucinates a brittle Jenga tower, go right ahead. But I suggest you grab a strong black coffee and try the East Coast Vibing approach and engineer a solution.


Sunday, August 16, 2026

SDK for jrm-code-project.com

A few of you have noticed the OpenAPI spec floating around the site lately. Rather than watching everyone write the same HTTP boilerplate from scratch to talk to the server, I went ahead and bundled up a set of official client bindings.

If you want to programmatically hit the pastebin or mess with the other endpoints, the jrm-code-client repository is live.

Right now, it includes complete SDKs for:

  • Common Lisp (obviously)
  • Emacs Lisp (naturally)
  • Python (seriously?)
  • Go (ugh)

They all handle the JWT authentication handshake natively and deserialize the JSON responses into proper language-specific structs/objects. You can stop raw-dogging it with curl (unless that's your thing).

Source is up on GitHub: jrm-code-project/jrm-code-client

Play nice with the rate limits.


Saturday, August 15, 2026

OpenAPI Access to jrm-code-project.com

It's a web site! It's a service! jrm-code-project.com has an OpenAPI specification and you can use it to generate client code in your favorite programming language (which is Lisp, right?). The OpenAPI specification is available at https://jrm-code-project.com/openapi.yaml. There are the following endpoints:

  • GET /api/v1/ping - Returns a simple "pong" response to test connectivity and verify your authentication tier.
  • POST /api/v1/echo - Accepts a JSON payload and returns the same payload in the response. For testing your client.
  • POST /api/v1/auth/token - Exchange your long-lived programmatic API key for a short-lived JWT Bearer token to authenticate secure requests.
  • GET /api/v1/pastes - Retrieve a paste's content by its ID (Publicly readable, no auth required).
  • POST /api/v1/pastes - Create a new code snippet paste (Requires JWT).
  • DELETE /api/v1/pastes - Delete a specific paste you own (Requires JWT).
  • GET /api/v1/user/pastes - List all non-expired pastes associated with your authenticated account (Requires JWT).
  • POST /api/v1/chef - Programmatic access to The Chef. Submit your raw Lisp code to be mercilessly roasted. (Requires JWT and a x-goog-api-key header with your Gemini API key).

I invite you to explore the API and see what you can build with it. If you have any questions or feedback, please don't hesitate to reach out to me at eval.apply@gmail.com.