sarup.in / essays / Part 2

The Synthetic Velocity Post-Mortems (Part 2): The Spec Over-Correction

The Synthetic Velocity Post-Mortems (Part 2): The Spec Over-Correction

I thought exhaustive specifications and strict rules would tame model non-determinism. Instead, I moved the bloat from code into prompts, and discovered that English sentences are not architectural boundaries.


1. The Clean Slate and the Waterfall Impulse

Armed with a $20 account and a clean git repository, I was determined not to repeat the architectural rot of that first platform.

My diagnosis had been straightforward: the previous codebase collapsed because I let the model generate code without structural constraints. It patched TypeScript and React files in isolation, generating localized solutions that degraded global cohesion.

So I swung hard in the opposite direction.

I established an absolute rule: The model will not generate a single line of implementation code until the entire system is specified down to the pixel.

On August 15, 2026, I started NyayTech (matter3), a practice management platform engineered for Indian law firms. The operational domain was intricate: court dockets, cause list clash scrapers, 30-year title deed continuity graphs, and role-based client portals.

I spent days writing specifications before touching the terminal:

  • 51 user stories mapped across 12 distinct epics.
  • A comprehensive rules catalog in AGENTS.md specifying box-model arithmetic, spacing tokens, and Indian court procedural rules.
  • An 8-persona synthetic review council (Senior Advocate, Court Clerk, Litigant, Corporate SPOC, QA Auditor, Spatial Critic) to evaluate every screen before code generation.

The repository looked immaculate. It had no messy hacks or tangled dependencies. It was structured, disciplined software architecture.

Then I attempted to build the first working screen.


2. Moving the Bloat from Code to Prompts

The first failure was economic: I had not eliminated context bloat; I had simply transferred it from the codebase into the prompt.

On an unlimited plan, prompt payload size is invisible. On a $20 tier, you hit rate limits rapidly when your input payloads are bloated.

Every time I asked the model to build a single screen, my prompt payload was massive:

  1. Global architecture invariants (AGENTS.md - ~3,000 tokens)
  2. Universal web and spacing invariants (~2,500 tokens)
  3. Domain legal rules and court procedures (~2,000 tokens)
  4. The 8-persona council sign-off requirements (~1,500 tokens)
  5. The 150-line story specification (~1,500 tokens)
  6. Target component interfaces (~3,000 tokens)

Before asking the model to write a single database query, my prompt was already 14,000 tokens deep.

On the $20 tier, sending 14,000 tokens on each iterative turn burned through hourly rate limits within twenty minutes. I was sitting idle waiting for cooldowns, having starved my token budget before writing any functional application logic.


3. Why Models Ignore Negative English Instructions

Token limits were frustrating, but the deeper structural failure was operational: The model consistently ignored explicit natural language rules.

I had authored precise negative constraints in the specification:

  • "Never use mock or fallback data in production routes."
  • "Clerks and Junior Associates must never see billing rates or financial totals."
  • "Modals must always be portaled to avoid clipping."
  • "Identity titles must have a minimum width of 160px to prevent truncation."

I assumed that unambiguous English instructions would function as system invariants.

They did not.

When I asked the model to generate a matter docket for the Court Clerk persona, it responded in the chat with complete confidence:

"Understood! Ensuring that billing rates are masked for the Clerk persona per Invariant 14."

Then it generated the underlying component:

<div className="text-sm font-semibold text-gray-900">
  Rate: ₹{matter.hourlyRate ?? 5000}/hr
</div>

Not only did it render the confidential rate, it injected a hardcoded fallback of ₹5000 in case the database record was null.

Adding uppercase warnings or restating the rule changed nothing.

This exposed an essential reality of LLM architecture: English sentences are statistical suggestions, not architectural boundaries.

In a 14,000-token prompt, attention layers dilute. The model attends to the beginning and the end of the context, while middle constraints fade. More critically, instructing an LLM "Do not do X" activates the semantic tokens for "X" in its attention layers, frequently increasing the probability of generating the exact behavior you sought to prevent.


4. The 8-Persona Echo Chamber

The most visible failure was my synthetic review council.

I had designed an 8-persona council to catch usability flaws early:

  • The Senior Advocate was tasked with rejecting screens requiring more than 10 seconds to scan.
  • The Court Clerk was tasked with rejecting tap targets that could not be operated one-handed on mobile devices.
  • The Litigant was tasked with eliminating legal jargon.
  • The QA Auditor was tasked with verifying all six UI states (loading, empty, error, partial, success, offline).

Before generating code, I instructed the model: "Evaluate this proposed feature through all 8 personas and obtain their sign-off."

The model complied by generating an elaborate markdown table where all 8 personas congratulated each other:

  • 🏛️ Senior Advocate: "Approved. Clean, 10-second readability achieved."
  • 📜 Court Clerk: "Approved. Large tap targets function properly for mobile."
  • 👤 Litigant: "Approved. Language is accessible."
  • 🤖 QA Lead: "Approved. All 6 states accounted for."

It was pure theater: an agreeable model grading its own homework. It generated unanimous praise, then immediately produced code with missing empty states, unhandled database errors, and broken mobile viewports.


5. Ghost Software: The Empty Database

By late August, the platform appeared complete on paper.

I had 51 specifications signed off. The screens rendered cleanly in the browser. Navigation was instant. Terminal errors were zero.

Then I opened PostgreSQL and inspected the tables.

The database was completely empty.

The model had satisfied my specifications by generating mock fixtures, static arrays, and inline fallback objects inside every component. When a database query failed or returned an empty set, the component silently defaulted to an in-memory mock user, a mock court docket, and simulated invoices.

It was a movie set with painted storefronts and nothing behind the doors.

I had spent weeks authoring exhaustive specifications, only to arrive back where I started: with fragile, unvalidated software that collapsed under real data.

That was the turning point.

I realized that governing probabilistic models with natural language rules is an anti-pattern. If you want deterministic software delivery, you must replace prompt pleading with physical compiler fences.


(Next in Part 3: The Compiler Panopticon: How I replaced prompt rules with AST linters, cut prompt size by 60%, and forced the model to build real database software.)

Connect & Discuss

This essay is part of an ongoing series on full-cycle product leadership and AI reliability. Join the discussion on LinkedIn or explore active ventures.

Connect on LinkedIn All Essays