DEV Community

Cover image for Taming the 5–10% Instability Rule: Using ASA for Deterministic Regeneration
vibecodiq
vibecodiq

Posted on • Originally published at vibecodiq.com

Taming the 5–10% Instability Rule: Using ASA for Deterministic Regeneration

If you use AI coding tools daily, you've probably seen this:

You ask the assistant to "update this endpoint", and the PR comes back with a lot more than that:

  • The service changed
  • A helper was "cleaned up"
  • A test file got rewritten
  • An unrelated import moved somewhere else

Nothing looks obviously wrong, but over time you accumulate a growing zone of "I don't know why this changed."

That's what I call the 5–10% instability rule:

Every regeneration quietly destabilizes ~5–10% of your codebase through unrequested changes.

In this post, I'll show how ASA v1.0 (a deterministic CLI for slice‑based architecture) turns regeneration from a risky operation into a fully controlled, local, repeatable process.


The concrete pain: AI touching more than you asked for

Typical flow today:

  1. You change requirements for login (extra field, new error).
  2. You ask AI: "Update the login endpoint, service and tests."
  3. The assistant edits 5–10 files — some relevant, some "for consistency."
  4. The diff includes signature changes, helper refactors, and test rewrites.

As an engineer, you now have three problems:

  • Unclear blast radius — what else did we just change?
  • Architecture erosion — a "small cleanup" breaks a domain boundary.
  • Fear of regeneration — doing this again might overwrite business logic.

You can try to tighten the prompt, but the real issue is that nothing in your project structure encodes strict boundaries plus regeneration rules.

ASA's core idea is: encode them.


ASA in 30 seconds

ASA v1.0 is a Python CLI that works on Slices: vertical feature units that colocate everything for one use‑case:

domains/auth/login/
├── slice.spec.md        # Human spec
├── slice.contract.json  # Machine contract
├── handler.py           # FastAPI endpoint
├── service.py           # Business logic
├── repository.py        # Data access
├── schemas.py           # Pydantic models
└── tests/
Enter fullscreen mode Exit fullscreen mode

The pipeline is deterministic:

slice.spec.md → slice.contract.json → skeleton → your implementation
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Slices give you local, vertical boundaries.
  • Specs & contracts are the single source of truth.
  • Markers preserve your implementation across regeneration.
  • Linter enforces domain boundaries with AST checks.

Before vs After: Changing a Slice

Let's walk through a real change: adding ip_address to login for security logging.

Before ASA: free‑form codebase

You might have something like:

# auth/routes.py
@router.post("/login")
def login(payload: dict):
    user = user_repo.get_by_email(payload["email"])
    if not user or not verify_password(payload["password"], user.password_hash):
        raise HTTPException(status_code=401)
    token = generate_jwt(user.id)
    audit_login(user.id)  # logging somewhere else
    return {"token": token}
Enter fullscreen mode Exit fullscreen mode

Now product asks: "Also store the IP address and make it available to an analytics job."

With AI, your prompt is vague by definition:

"Update login to accept ip_address, store it, and keep tests passing."

The assistant might:

  • Change this handler
  • Rewrite some helper called audit_login
  • Adjust a couple of tests
  • Maybe "simplify" a repository interface on the way

You get your feature — and another 5–10% of "not sure what changed".


After ASA: slice‑based, deterministic workflow

In ASA, login lives in its own Slice:

domains/auth/login/
  slice.spec.md
  slice.contract.json
  handler.py
  service.py
  repository.py
  schemas.py
  tests/
Enter fullscreen mode Exit fullscreen mode

Step 1 — Update the Spec (domains/auth/login/slice.spec.md):

# Purpose
User logs in with email and password. Returns JWT token.

## Inputs
- email: string
- password: string
- ip_address: string

## Outputs
- jwt_token: string
- expires_in: int

## Behaviour
- Verify user exists.
- Verify password.
- Generate JWT token.
- Log IP address for security.

## Errors
- INVALID_CREDENTIALS: Invalid email or password.

## Side Effects
- Logs login attempt with IP address.

## Dependencies
None
Enter fullscreen mode Exit fullscreen mode

Step 2 — Regenerate contract + skeleton:

asa generate-contract auth/login
asa regenerate-slice auth/login
Enter fullscreen mode Exit fullscreen mode

generate-contract updates slice.contract.json from the spec.
regenerate-slice then reads the contract and updates the Python files:

  • schemas.py (adds ip_address)
  • handler/service/repository structure
  • tests skeleton

Step 3 — Verify your implementation survived

Business logic in service.py is wrapped by markers:

from .repository import LoginRepository
from .schemas import LoginRequest, LoginResponse

class LoginService:
    def __init__(self) -> None:
        self.repo = LoginRepository()

    # === BEGIN USER CODE ===
    def execute(self, request: LoginRequest) -> LoginResponse:
        user = self.repo.get_user_by_email(request.email)
        if not user:
            raise ValueError("USER_NOT_FOUND")
        if not self.repo.verify_password(user, request.password):
            raise ValueError("INVALID_CREDENTIALS")

        token = generate_jwt(user.id)
        self.repo.log_login(user.id, request.ip_address)

        return LoginResponse(jwt_token=token, expires_in=3600)
    # === END USER CODE ===
Enter fullscreen mode Exit fullscreen mode

ASA regenerates the file around this block and then reinserts your function body exactly as written.

You get:

  • Updated schemas and contracts
  • Same business logic
  • No surprise changes in unrelated slices

Step 4 — Run the linter:

asa lint auth/login
Enter fullscreen mode Exit fullscreen mode

If AI tried to sneak in a cross‑domain import (e.g. directly calling domains.analytics), the linter fails and tells you exactly where.


Minimal CLI workflow to adopt ASA

To start using ASA on a new feature, you typically do:

# 1) Create a slice
asa create-slice auth/login

# 2) Write the spec (manual or with AI help)
#    edit domains/auth/login/slice.spec.md

# 3) Generate contract + skeleton
asa generate-contract auth/login
asa generate-skeleton auth/login

# 4) Implement your logic between markers in service.py/repository.py

# 5) Lint before committing
asa lint auth/login
Enter fullscreen mode Exit fullscreen mode

From then on, any change to login flows through:

asa generate-contract auth/login
asa regenerate-slice auth/login
asa lint auth/login
Enter fullscreen mode Exit fullscreen mode

Regeneration becomes a routine operation, not a roll of the dice.


Why this kills the 5–10% instability rule

The 5–10% rule thrives on three things:

  1. Global, shared helpers
  2. Fuzzy boundaries ("we try not to import that…")
  3. Non‑deterministic generation

ASA attacks all three:

  • Slices encourage local duplication of business helpers instead of sharing them across domains.
  • Domain rules are enforced by an AST linter, not just by code review.
  • The CLI is deterministic — same spec, same skeleton, every time.

AI is still in the loop, but its freedom is constrained by contracts, markers and lint rules.

You keep the speed. You lose the architectural roulette.


If you want to see the full starter kit and CLI in action, check out:

Source code & starter kit: https://github.com/vibecodiq/asa-starter-kit

Top comments (0)