Infrastrutture da Incubo – Episodio 5
30 Ottobre 2026 - 8 min. read
Damiano Giorgi
DevOps Engineer

Il refactoring del codice legacy rappresenta uno dei colli di bottiglia più complessi e onerosi nella manutenzione del software enterprise. L'avvento dell'Intelligenza Artificiale Generativa ha promesso di risolvere il problema traducendo e ottimizzando intere codebase in pochi secondi. Tuttavia, nell'ingegneria del software reale, limitarsi ad accelerare la scrittura del codice introduce spesso un problema nuovo e letale: l'effetto domino delle allucinazioni architetturali.
Questo articolo analizza l'adozione dello Spec-Driven Development (SDD) tramite il workflow nativo artifact-driven di Kiro. Sfruttando strutture rigorose di requisiti, documenti di design tecnico e agent hook, i team di ingegneria possono trasformare i test automatici nel guardrail definitivo contro le regressioni, creando al tempo stesso un linguaggio comune tra stakeholder non tecnici e software engineer.
Quando a un Large Language Model (LLM) tradizionale viene chiesto di "effettuare il refactoring di una funzione per migliorarne le prestazioni e la leggibilità", l'algoritmo ottimizza la probabilità stilistica e la concisione, non la correttezza semantica o l'aderenza al contesto di business.
Il rischio del codice "apparentemente perfetto"
I principali pericoli dell'allucinazione nelle attività di refactoring sono:
Si consideri questo modulo JavaScript/TypeScript legacy, privo di annotazioni di tipo rigorose e di pattern async moderni:
// LEGACY CODE: Transaction handling with implicit dependencies
// and missing types
function processPaymentLegacy(userId, amount, currency, retryCount = 0) {
if (amount <= 0) {
return { status: "FAILED", code: 400 };
}
// Historical bug: API v1 only accepts float strings formatted with 2
// decimal places
const formattedAmount = Number(amount).toFixed(2);
try {
const res = legacyGatewayCall(userId, formattedAmount, currency);
if (res.code === 200) {
return { status: "SUCCESS", txId: res.txId };
} else if (res.code === 503 && retryCount < 3) {
return processPaymentLegacy(userId,amount,currency,retryCount+1);
}
} catch (e) {
logError(e);
}
return { status: "FAILED", code: 500 };
}
Un prompt generico inviato a un LLM non guidato produce spesso un risultato TypeScript di questo tipo: dall'aspetto moderno, ma “rotto”:
// HALLUCINATED REFACTORING (Modern TypeScript syntax, but logically broken)
export async function processPayment(
userId: string,
amount: number,
currency: string): Promise<object> {
if (amount <= 0) {
// BREAKS CONTRACT: Throws exception instead of
// returning a status object
throw new Error("Invalid amount");
}
// HALLUCINATION: Removes the 2-decimal string formatting required by
// legacy gateway
const response = await legacyGatewayCall(userId, amount, currency);
// HALLUCINATION: Completely removes the recursive retry logic
// for "cleanliness"
if (response.code === 200) {
return { status: "SUCCESS", txId: response.txId };
}
return { status: "FAILED", code: response.code };
}
Per superare l'ambiguità che genera le allucinazioni, Kiro implementa un workflow di Spec-Driven Development (SDD) nativo e rigoroso. Invece di affidarsi a prompt conversazionali o a framework di test generici, Kiro utilizza una gerarchia predefinita di artefatti markdown come unica fonte di verità. Questo impedisce all'AI di formulare assunzioni e vincola in modo stringente il suo comportamento in fase di refactoring.
# Legacy Payment Processing Requirements
1. **Precision Constraint:**
WHERE a transaction is initiated, THE SYSTEM SHALL format the payment amount as a string with exactly two decimal places before transmitting it to the legacy gateway.
2. **Resilience Strategy:**
WHEN the legacy gateway returns a 503 HTTP status code, THE SYSTEM SHALL retry the request up to 3 times.
3. **Contract Adherence:**
IF the transaction fails after all retries, THE SYSTEM SHALL return a JSON dictionary containing `{"status": "FAILED"}` and SHALL NOT raise an unhandled exception.
Kiro opera attraverso un Agentic Execution Loop test-driven, strettamente accoppiato al file tasks.md e ai test automatici del repository.
Kiro preleva un task di refactoring, applica le modifiche al codice e quindi esegue un Agent Hook. Se l'hook (che lancia la test suite) fallisce, Kiro cattura lo stack trace, lo reimmette nella propria context window e tenta di correggere l'allucinazione o l'errore logico. Passa al task successivo solo quando i test risultano superati.
Per abilitare l'approccio Zero-Regression è necessario configurare i confini operativi di Kiro tramite la sua struttura nativa di directory / file all'interno del repository. Di seguito il dettaglio delle directory e dei file necessari.
Struttura del repository
my-enterprise-service/
├── .kiro/
│ ├── steering/ # Steering Rules
│ │ ├── architecture.md
| | ├── design.md
| | └── requirements.md
│ ├── hooks/ # Agent Hooks
│ └── post-task.json
| └── specs/ # Spec Tasks
│ └── tasks.md
├── defer" defer" src/
│ └── paymentService.ts
└── tests/
└── payment-service.spec.ts1. Steering Rule (.kiro/steering/architecture.md)
Le steering rule sono file markdown che definiscono vincoli validi per l'intero team. Kiro le acquisisce e le applica globalmente a ogni task.
# Architectural Steering Rules
1. **Immutability**: All domain data structures and DTOs must be immutable.
2. **Exceptions**: Do not throw unhandled exceptions from service layers. Always respect the HTTP `Result` envelope pattern.
3. **Logging**: Use `@aws-lambda-powertools/logger`. Never log clear-text PAN or PII data.
2. Requisiti di business (.kiro/steering/requirements.md)
Proseguendo con l'esempio precedente, ecco un insieme valido di requisiti di business scritti in sintassi EARS:
# Legacy Payment Processing Requirements
1. **Precision Constraint:**
WHERE a transaction is initiated, THE SYSTEM SHALL format the payment amount as a string with exactly two decimal places before transmitting it to the legacy gateway.
2. **Resilience Strategy:**
WHEN the legacy gateway returns a 503 HTTP status code, THE SYSTEM SHALL retry the request up to 3 times.
3. **Contract Adherence:**
IF the transaction fails after all retries, THE SYSTEM SHALL return a JSON dictionary containing `{"status": "FAILED"}` and SHALL NOT raise an unhandled exception.
3. Design tecnico (.kiro/steering/design.md)
Dopo aver revisionato requirements.md, Kiro genera (o riceve già pronto) design.md per mappare la scomposizione tecnica dei componenti, le interfacce e la gestione degli errori prima di toccare qualsiasi file sorgente:
# Design Specification: Payment Service Refactoring
## 1. Architecture Overview
Refactor the legacy `processPaymentLegacy` function into an asynchronous, strongly-typed TypeScript module (`processPayment`) adhering to `@aws-lambda-powertools/logger`.
## 2. Interface Contracts
```typescript
export interface PaymentRequest {
readonly userId: string;
readonly amount: number;
readonly currency: string;
}
export interface PaymentResponse {
readonly status: "SUCCESS" | "FAILED";
readonly txId?: string;
readonly errorCode?: number;
}
4. Flusso dei dati e strategia di retry (.kiro/specs/task.md)
# Implementation Plan
## Overview
Refactor the legacy payment processing code into a strongly-typed, async TypeScript module that satisfies the precision, resilience, and contract-adherence requirements.
## Tasks
- [ ] 1. Create TypeScript Types & Interfaces
- Define `PaymentRequest` and `PaymentResponse` inside `src/types.ts`.
- Enforce immutability using `readonly` modifiers per `.kiro/steering/architecture.md`.
- [ ] 2. Refactor Payment Execution Module
- Refactor `src/payment-service.ts` to implement the async `processPayment` function.
- Implement two-decimal string formatting constraint for amount payloads.
- Implement 503 retry resilience logic.
- [ ] 3. Run Post-Task Verification Hook
- Execute `.kiro/hooks/post-task.json` to ensure `tests/payment-service.spec.ts` passes.
## Task Dependency Graph
```json
{
"waves": [
{
"wave": 1,
"tasks": [1]
},
{
"wave": 2,
"tasks": [2],
"dependsOn": [1]
},
{
"wave": 3,
"tasks": [3],
"dependsOn": [2]
}
]
}
```
## Notes
- Task 2 depends on the interfaces defined in Task 1; complete them in order.
- Task 3 is a verification step and should only run after Task 2 is complete.
- All domain structures must use `readonly` modifiers (immutability rule).
- Do not throw unhandled exceptions from the service layer; return `{"status": "FAILED"}` on exhausted retries.
- Never log clear-text PAN or PII data.
5. Agent Hook (.kiro/hooks/post-task.json)
Gli agent hook sono script eseguiti in corrispondenza di specifici eventi del ciclo di vita. L'hook post-task è cruciale per lo Zero-Regression: esegue automaticamente la test suite non appena Kiro ritiene di aver completato uno step di refactoring.
{
"version": "v1",
"hooks": [
{
"name": "Post-Task Test Verification",
"trigger": "PostTaskExec",
"description": "Runs the Jest test suite after each spec task completes. Exits with code 1 on failure so Kiro can detect regression and self-correct.",
"action": {
"type": "command",
"command": "cd \"/Users/alessandrogaggia/Projects/Kiro Article\" && npm test -- --json --outputFile=test-results.json; if [ $? -ne 0 ]; then echo \"Tests failed. Analyze test-results.json and correct the code.\"; exit 1; fi; echo \"Tests passed. Zero regression verified.\""
}
}
]
}
Ripercorriamo il refactoring del nostro modulo di pagamento legacy utilizzando l'SDD di Kiro.
Inizialmente uno sviluppatore scrive un test rigoroso basato su requirements.md:
// tests/payment.spec.ts
import { processPayment } from '../src/paymentService';
describe('Payment Service Contract', () => {
it('should guarantee exact float precision and retry behavior', async () => {
// Mock temporary 503 Gateway failure
jest.spyOn(global, 'fetch')
.mockResolvedValueOnce(new Response(null, { status: 503 }))
.mockResolvedValueOnce(new Response(JSON.stringify({ txId: "TX-123" }), { status: 200 }));
const result = await processPayment({ userId: "USR-1", amount: 19.99, currency: "EUR" });
expect(result.status).toBe("SUCCESS");
expect(fetch).toHaveBeenCalledTimes(2); // Verifies retry occurred
});
});Ora siamo pronti ad avviare Kiro. Esistono diversi modi per chiedergli di iniziare a lavorare sulla task list.
Metodo 1: Kiro Spec Panel (pulsante UI)
Nella sidebar della chat di Kiro, referenziare direttamente il file dei task tramite @tasks.md:
@tasks.md Execute Task 1
oppure
Start working on Task 1 from tasks.md following our steering rules.
Metodo 3: modalità agente autonomo di Kiro (Autopilot)
Quando Kiro esegue i task definiti in tasks.md, il file mostra il loop agentico in azione:
Dopo ogni task tenta di lanciare i test, ne verifica la struttura e la corregge quando necessario:
Dopo ogni task tenta di lanciare i test, ne verifica la struttura e la corregge quando necessario:
Terminate tutte le modifiche a spec e dati, esegue l'hook richiesto:
Una volta completati tutti i task, ecco il codice validato e sottoposto a refactoring:
// defer" defer" src/types.ts (Added per task's request)
/**
* Domain types for the payment service.
* All fields are readonly to enforce immutability per architecture rules.
*/
export interface PaymentRequest {
readonly userId: string;
readonly amount: number;
readonly currency: string;
}
export interface PaymentResponse {
readonly status: "SUCCESS" | "FAILED";
readonly txId?: string;
readonly code?: number;
}
// defer" defer" src/paymentService.ts (Refactored by Kiro & Validated by Hooks)
// Refactored payment service
// - Async, strongly-typed, fetch-based
// - Amount formatted to 2 decimal places (requirements.md §1)
// - 503 retry up to 3 times (requirements.md §2)
// - Never throws; always returns Result envelope (requirements.md §3, architecture.md §2)
// - No PAN/PII logged (architecture.md §3)
import { Logger } from "@aws-lambda-powertools/logger";
import { PaymentRequest, PaymentResponse } from "./types";
const logger = new Logger({ serviceName: "payment-service" });
const LEGACY_GATEWAY_URL =
process.env.LEGACY_GATEWAY_URL ?? "https://gateway.internal/v1/charge";
const MAX_RETRIES = 3;
export async function processPayment(
request: PaymentRequest
): Promise<PaymentResponse> {
return executeWithRetry(request, 0);
}
async function executeWithRetry(
request: PaymentRequest,
attempt: number
): Promise<PaymentResponse> {
// Requirement §1: format amount as string with exactly 2 decimal places
const formattedAmount = request.amount.toFixed(2);
try {
const response = await fetch(LEGACY_GATEWAY_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
userId: request.userId,
amount: formattedAmount,
currency: request.currency,
}),
});
if (response.ok) {
const body = (await response.json()) as { txId?: string };
return { status: "SUCCESS", txId: body.txId };
}
// Requirement §2: retry on 503 up to MAX_RETRIES times
if (response.status === 503 && attempt < MAX_RETRIES) {
logger.warn("Gateway returned 503, retrying", {
attempt,
currency: request.currency,
});
return executeWithRetry(request, attempt + 1);
}
// All retries exhausted or non-retryable failure — requirement §3
logger.error("Gateway call failed, returning FAILED", {
statusCode: response.status,
attempt,
currency: request.currency,
});
return { status: "FAILED", code: response.status };
} catch (error) {
// Architecture rule §2: never propagate unhandled exceptions from service layer
// Architecture rule §3: log error reference only, no PII
logger.error("[payment-service] Gateway call threw an error", {
attempt,
currency: request.currency,
});
return { status: "FAILED", code: 500 };
}
}Per garantire che l'integrazione di Kiro funzioni su scala enterprise, l'ambiente di esecuzione deve essere integrato con i servizi nativi AWS per l'isolamento dei test e la sicurezza, un esempio comune può essere:
Ecco una semplice tabella che mette a confronto il refactoring tradizionale, il refactoring guidato da prompt generici e il refactoring SDD con Kiro:
| Caratteristica | Refactoring manuale tradizionale | Copilot / prompt AI generico | Spec-Driven Refactoring con Kiro |
|---|---|---|---|
| Velocità di esecuzione | Molto bassa (giorni/settimane) | Molto alta (secondi) | Alta (minuti, guidata dagli hook) |
| Rischio di allucinazioni | Nullo (umano) | Molto alto (nessun guardrail) | Nullo / trascurabile (intercettato dai test) |
| Artefatti di contesto | Pagine wiki, ticket Jira | Prompt in chat | requirements.md, design.md, tasks.md |
| Verifica | Manuale tramite PR | Assente / Manuale | Automatica tramite Agent Hook |
| Loop di correzione | Manuale | Manuale (nuovo prompt dell'utente) | Autonomo (Agentic Self-Correction) |
Nonostante l'automazione avanzata di Kiro, il ruolo del Tech Lead resta centrale per la sicurezza del sistema. Lo "Zero-Regression" non elimina la revisione umana, ma ne cambia radicalmente la natura:
Adottando questo workflow, il Lead Dev si trasforma da collo di bottiglia a supervisore di un processo agentico guidato da dati oggettivi in formato markdown.
Adottare l'AI per il refactoring non deve significare vivere nel timore costante di regressioni sottili. Abbracciando lo Spec-Driven Development con Kiro, si trasformano artefatti eseguibili (requirements.md, design.md, tasks.md) e hook post-task automatici in guardrail immutabili. Questo eleva l'AI generativa da caotico strumento di completamento a partner ingegneristico disciplinato e auto-correttivo, permettendo al team di eliminare il debito tecnico garantendo al tempo stesso un'approccio Zero-Regression.