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:
- You change requirements for login (extra field, new error).
- You ask AI: "Update the login endpoint, service and tests."
- The assistant edits 5–10 files — some relevant, some "for consistency."
- 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/
The pipeline is deterministic:
slice.spec.md → slice.contract.json → skeleton → your implementation
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}
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/
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
Step 2 — Regenerate contract + skeleton:
asa generate-contract auth/login
asa regenerate-slice auth/login
generate-contract updates slice.contract.json from the spec.
regenerate-slice then reads the contract and updates the Python files:
-
schemas.py(addsip_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 ===
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
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
From then on, any change to login flows through:
asa generate-contract auth/login
asa regenerate-slice auth/login
asa lint auth/login
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:
- Global, shared helpers
- Fuzzy boundaries ("we try not to import that…")
- 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)