Spec Drift Detection Golang: Deteksi Kode yang Keluar dari Spesifikasi
Cara mendeteksi dan mencegah spec drift dalam proyek Golang SDD. Teknik otomatis dan manual untuk memastikan kode tetap sesuai spesifikasi sepanjang siklus development.
Spec Drift Detection: Ketika Kode Keluar dari Spesifikasi
Spec drift detection di Golang adalah kemampuan mendeteksi fenomena ketika kode perlahan-lahan menyimpang dari spesifikasi yang telah ditetapkan — bukan karena perubahan yang disengaja, tapi karena akumulasi keputusan kecil yang tidak pernah di-trace kembali ke spec.
Tanda-tandanya halus: sebuah error code yang berubah nama, sebuah field response yang dihilangkan karena “sepertinya tidak diperlukan,” sebuah edge case yang di-skip karena deadline mendekat. Masing-masing kecil. Tapi bersama-sama, mereka menciptakan jarak antara apa yang spec katakan dan apa yang kode lakukan.
Di artikel ini, kita bahas bagaimana mendeteksi spec drift sebelum menjadi masalah production, dan bagaimana membangun mekanisme pencegahan yang berkelanjutan — dari test sebagai spec guard sampai custom linter dan CI check.
17.1 Penyebab Spec Drift yang Paling Umum
Sebelum mendeteksi, kita perlu paham dari mana drift biasanya berasal. Ada empat penyebab yang paling sering muncul di proyek nyata.
Penyebab 1: “Ini Perbaikan Kecil”
Penyebab paling umum adalah perubahan yang dianggap sepele sehingga spec tidak ikut diperbarui. Potongan Go berikut memperlihatkan bagaimana satu “perbaikan kecil” mengubah error code tanpa menyentuh spec.
1// Kode asli (sesuai spec AC9):
2return c.JSON(http.StatusConflict, ErrorResponse{
3 ErrorCode: "ORDER_NOT_CANCELLABLE",
4 Message: err.Error(),
5})
6
7// "Perbaikan kecil" tanpa update spec:
8return c.JSON(http.StatusConflict, ErrorResponse{
9 ErrorCode: "NOT_CANCELLABLE", // ← error code berubah, spec tidak di-update
10 Message: err.Error(),
11})Perubahan yang terlihat kosmetik ini punya dampak nyata: setiap consumer yang bergantung pada string "ORDER_NOT_CANCELLABLE" akan langsung break, padahal tidak ada satu pun error di sisi kode ini.
Penyebab 2: Spec yang Tidak Diupdate Setelah Diskusi
Penyebab kedua muncul ketika keputusan diambil di ruang meeting tapi hanya dicatat di kode, bukan di spec. Skenario berikut menggambarkan bagaimana keputusan yang valid pun bisa menghasilkan drift.
1Sprint planning:
2"Kita ubah timeout dari 15 menit ke 30 menit berdasarkan user research."
3[Semua setuju, keputusan dicatat di Slack]
4[Kode diupdate, spec tidak diupdate]
5
6Dua bulan kemudian:
7"Kenapa spec bilang 15 menit tapi kode pakai 30 menit?"Pelajarannya: keputusan tim tidak dianggap “selesai” sampai spec ikut diperbarui — Slack bukan sumber kebenaran yang bisa di-trace berbulan-bulan kemudian.
Penyebab 3: Copy-Paste Coding
Penyebab ketiga adalah kebiasaan menyalin handler yang sudah ada lalu lupa menyesuaikan detail yang seharusnya berbeda. Contoh berikut menunjukkan error code yang ikut tersalin dari spec yang salah.
1Developer copy handler yang sudah ada, lupa update error code:
2
3CreateOrder handler error:
4ErrorCode: "INVALID_REQUEST" ← sesuai create-order.md
5
6Cancel handler (hasil copy-paste):
7ErrorCode: "INVALID_REQUEST" ← harusnya sesuai cancel-order.md, mungkin bedaIntinya: copy-paste menghemat waktu menulis tapi menanam risiko drift, karena nilai yang ikut tersalin belum tentu cocok dengan spec fitur yang baru.
Penyebab 4: Berbeda Interpretasi AC
Penyebab keempat muncul saat spec ambigu sehingga tiap developer menafsirkannya berbeda. Contoh berikut memperlihatkan tiga interpretasi yang semuanya “masuk akal”.
1Spec: "System memvalidasi bahwa cart milik user yang authenticated"
2
3Developer A: return 404 jika cart bukan milik user (bisa reveal existence)
4Developer B: return 422 (sesuai spec AC9 di create-order.md)
5Developer C: return 403 (paling "semantically correct" menurut REST)
6
7Semuanya punya justifikasi, tapi spec tidak eksplisit → driftKesimpulannya: drift jenis ini bukan salah developer melainkan salah spec — obatnya adalah menutup ambiguitas di spec, bukan menyalahkan interpretasi.
17.2 Deteksi Manual: Spec Compliance Audit
Pendekatan paling basic adalah audit manual secara berkala. Template audit berikut memetakan tiap AC ke apa yang benar-benar dilakukan kode, sehingga drift langsung terlihat dalam satu tabel.
1# Spec Compliance Audit
2# Date: 2025-07-15
3# Feature: Cancel Order
4# Spec version: cancel-order.md v1.3
5# Auditor: @budi
6
7## AC Compliance
8
9| AC | Spec Says | Code Does | Status | Notes |
10|----|-----------|-----------|--------|-------|
11| AC6 | Return 204 No Content | Returns 204 ✓ | ✅ | |
12| AC7 | ErrOrderNotFound → 404 | Returns 404 ✓ | ✅ | |
13| AC9 | error_code: ORDER_NOT_CANCELLABLE | error_code: NOT_CANCELLABLE | ❌ | Drift found! |
14| AC10 | error_code: CANCEL_WINDOW_EXPIRED | error_code: WINDOW_EXPIRED | ❌ | Drift found! |
15
16## Action Items
171. Fix error codes di handler (PR #234)
182. Update comment di spec bahwa ini ditemukan via audit
193. Add test that explicitly assert error code stringsKekuatan audit manual ada pada kolom Action Items: begitu drift ditemukan, ia langsung diterjemahkan menjadi tugas konkret dengan nomor PR — bukan sekadar catatan yang menguap.
17.3 Deteksi Otomatis: Test sebagai Spec Guard
Audit manual tidak scalable, jadi lini pertahanan utama sebaiknya otomatis. Test yang paling efektif adalah yang secara eksplisit assert string value yang ada di spec, seperti test berikut untuk AC9.
1// cancel_order_handler_test.go
2
3// TestCancelOrder_AC9_ErrorCodeMatchesSpec verifies that the exact error code
4// from spec AC9 is returned when order is not cancellable.
5// SPEC: cancel-order.md v1.3 AC9: error_code must be "ORDER_NOT_CANCELLABLE"
6func TestCancelOrder_AC9_ErrorCodeMatchesSpec(t *testing.T) {
7 // Setup: confirmed order
8 handler, mockUC := setupHandler(t)
9 mockUC.EXPECT().Execute(gomock.Any(), gomock.Any()).
10 Return(&cancelorder.OrderNotCancellableError{
11 CurrentStatus: "CONFIRMED",
12 })
13
14 req := newCancelRequest(t, uuid.New())
15 rec := httptest.NewRecorder()
16 handler.CancelOrder(echo.New().NewContext(req, rec))
17
18 // Assert HTTP status
19 assert.Equal(t, http.StatusConflict, rec.Code)
20
21 // Assert EXACT error_code from spec — this prevents drift
22 var body map[string]interface{}
23 json.Unmarshal(rec.Body.Bytes(), &body)
24 assert.Equal(t, "ORDER_NOT_CANCELLABLE", body["error_code"],
25 "error_code must match spec AC9 exactly: 'ORDER_NOT_CANCELLABLE'")
26 // Note: this test will fail if someone changes the error code
27}Karena test ini mem-assert string persis dari spec, setiap perubahan error code di kode akan otomatis gagal di CI — mustahil lolos tanpa menyentuh test, dan dengan begitu developer pasti sadar sedang mengubah kontrak.
17.4 Deteksi Otomatis: Spec Assertion Helper
Agar assertion terhadap spec konsisten di seluruh test, buat helper khusus yang menyimpan referensi spec. Struct SpecAssert berikut membungkus pengecekan error code dan HTTP status sekaligus mencatat versi spec-nya.
1// internal/testing/spec_helper.go
2
3// SpecAssert helps assert that code behavior matches spec.
4// Usage: ensures tests are tied to specific spec versions.
5type SpecAssert struct {
6 t *testing.T
7 specRef string // e.g., "cancel-order.md v1.3 AC9"
8}
9
10func NewSpecAssert(t *testing.T, specRef string) *SpecAssert {
11 return &SpecAssert{t: t, specRef: specRef}
12}
13
14// ErrorCode asserts that the error code in the response matches spec.
15func (sa *SpecAssert) ErrorCode(got, expected string) {
16 sa.t.Helper()
17 if got != expected {
18 sa.t.Errorf(
19 "SPEC DRIFT DETECTED [%s]\n"+
20 "Expected error_code: %q\n"+
21 "Got: %q\n"+
22 "Spec ref: %s",
23 sa.specRef, expected, got, sa.specRef)
24 }
25}
26
27// HTTPStatus asserts that the HTTP status matches spec.
28func (sa *SpecAssert) HTTPStatus(got, expected int) {
29 sa.t.Helper()
30 if got != expected {
31 sa.t.Errorf(
32 "SPEC DRIFT DETECTED [%s]\n"+
33 "Expected HTTP status: %d\n"+
34 "Got: %d",
35 sa.specRef, expected, got)
36 }
37}Helper ini menyematkan specRef di setiap pesan error, sehingga saat test gagal, developer langsung tahu spec dan AC mana yang dilanggar. Pemakaiannya di test menjadi ringkas dan self-documenting seperti berikut.
1func TestCancelOrder_AC9(t *testing.T) {
2 spec := testinghelper.NewSpecAssert(t, "cancel-order.md v1.3 AC9")
3
4 // ... setup dan eksekusi ...
5
6 spec.HTTPStatus(rec.Code, http.StatusConflict)
7 spec.ErrorCode(body["error_code"].(string), "ORDER_NOT_CANCELLABLE")
8}Dengan pola ini, setiap baris assertion membaca seperti kalimat spec: “status harus Conflict, error code harus ORDER_NOT_CANCELLABLE”. Ketika drift terjadi, output-nya pun sangat jelas seperti berikut.
1SPEC DRIFT DETECTED [cancel-order.md v1.3 AC9]
2Expected error_code: "ORDER_NOT_CANCELLABLE"
3Got: "NOT_CANCELLABLE"
4Spec ref: cancel-order.md v1.3 AC9Output yang menyebut nama spec, AC, nilai yang diharapkan, dan nilai aktual sekaligus membuat triase drift memakan hitungan detik, bukan menit.
17.5 Deteksi via OpenAPI Contract Test
Jika sudah punya OpenAPI spec, kita bisa memakainya sebagai sumber kebenaran otomatis. Test berikut memvalidasi setiap response handler terhadap dokumen OpenAPI, sehingga penyimpangan format langsung tertangkap.
1// internal/delivery/http/handler/contract_test.go
2
3// TestAllEndpoints_ConformToOpenAPISpec verifies all handlers
4// return responses that conform to the OpenAPI specification.
5func TestAllEndpoints_ConformToOpenAPISpec(t *testing.T) {
6 // Load OpenAPI spec
7 loader := openapi3.NewLoader()
8 doc, err := loader.LoadFromFile("../../../../api/openapi.yaml")
9 require.NoError(t, err)
10 require.NoError(t, doc.Validate(loader.Context))
11
12 router, _ := legacyrouter.NewRouter(doc)
13
14 testCases := []struct {
15 name string
16 request func() *http.Request
17 expectedCode int
18 specRef string
19 }{
20 {
21 name: "Cancel order - success",
22 request: cancelOrderRequest(pendingOrderID, validJWT),
23 expectedCode: http.StatusNoContent,
24 specRef: "cancel-order.md AC6",
25 },
26 {
27 name: "Cancel order - not found",
28 request: cancelOrderRequest(nonExistentID, validJWT),
29 expectedCode: http.StatusNotFound,
30 specRef: "cancel-order.md AC7",
31 },
32 }
33
34 for _, tc := range testCases {
35 t.Run(tc.name, func(t *testing.T) {
36 rec := httptest.NewRecorder()
37 server.ServeHTTP(rec, tc.request())
38
39 // 1. Assert expected status code
40 assert.Equal(t, tc.expectedCode, rec.Code, "spec: %s", tc.specRef)
41
42 // 2. Validate against OpenAPI spec
43 resp := rec.Result()
44 route, pathParams, _ := router.FindRoute(tc.request())
45 err := openapi3filter.ValidateResponse(
46 context.Background(),
47 &openapi3filter.ResponseValidationInput{
48 RequestValidationInput: &openapi3filter.RequestValidationInput{
49 Request: tc.request(),
50 PathParams: pathParams,
51 Route: route,
52 },
53 Status: resp.StatusCode,
54 Header: resp.Header,
55 Body: resp.Body,
56 },
57 )
58 assert.NoError(t, err, "response must conform to OpenAPI spec [%s]", tc.specRef)
59 })
60 }
61}Keunggulan pendekatan ini: kita tidak perlu menulis assertion field per field secara manual — ValidateResponse membandingkan seluruh struktur response dengan OpenAPI spec, jadi field yang hilang atau bertipe salah langsung ketahuan.
17.6 Deteksi via Static Analysis Custom Linter
Deteksi bisa didorong lebih awal lagi, yaitu saat lint sebelum kode dijalankan. Custom linter golangci-lint berikut memastikan setiap error code yang muncul di handler sudah terdaftar di registry spec.
1// tools/linter/error_code_checker.go
2
3// ErrorCodeChecker verifies that all error codes used in handlers
4// are registered in the spec registry.
5type ErrorCodeChecker struct {
6 pass *analysis.Pass
7}
8
9var AllowedErrorCodes = map[string]string{
10 // Cancel Order spec
11 "ORDER_NOT_FOUND": "cancel-order.md AC7",
12 "ORDER_NOT_CANCELLABLE": "cancel-order.md AC9",
13 "CANCEL_WINDOW_EXPIRED": "cancel-order.md AC10",
14 // Create Order spec
15 "CART_EMPTY": "create-order.md AC10",
16 "INSUFFICIENT_STOCK": "create-order.md AC12",
17 "PRODUCT_UNAVAILABLE": "create-order.md AC11",
18 // General
19 "UNAUTHORIZED": "auth.md AC1",
20 "INVALID_REQUEST": "common.md",
21 "INTERNAL_ERROR": "common.md",
22}
23
24// Analyzer checks that all error code string literals in handlers
25// are in the allowed list.
26var Analyzer = &analysis.Analyzer{
27 Name: "errorcodecheck",
28 Doc: "checks that error codes match spec-registered values",
29 Run: run,
30}
31
32func run(pass *analysis.Pass) (interface{}, error) {
33 for _, file := range pass.Files {
34 // Check if this is a handler file
35 if !strings.HasSuffix(pass.Fset.File(file.Pos()).Name(), "_handler.go") {
36 continue
37 }
38
39 ast.Inspect(file, func(n ast.Node) bool {
40 lit, ok := n.(*ast.BasicLit)
41 if !ok || lit.Kind != token.STRING {
42 return true
43 }
44
45 value := strings.Trim(lit.Value, `"`)
46 // Check if this looks like an error code (UPPER_SNAKE_CASE)
47 if isErrorCodePattern(value) {
48 if _, allowed := AllowedErrorCodes[value]; !allowed {
49 pass.Reportf(lit.Pos(),
50 "unregistered error code %q — add to AllowedErrorCodes in spec registry",
51 value)
52 }
53 }
54 return true
55 })
56 }
57 return nil, nil
58}Peta AllowedErrorCodes menjadikan registry error code sebagai single source of truth: memperkenalkan error code baru memaksa developer mendaftarkannya beserta referensi spec, sehingga drift lewat “error code liar” tidak mungkin terjadi diam-diam.
17.7 Deteksi via Spec Coverage Report
Selain mencocokkan nilai, kita juga ingin tahu berapa banyak AC yang benar-benar diuji. Script berikut menghitung “spec coverage” — berapa persen AC dan EC yang punya test yang mereferensikannya secara eksplisit.
1#!/bin/bash
2# scripts/spec-coverage.sh
3
4SPEC_FILE="specs/order/cancel-order.md"
5TEST_DIR="internal"
6
7echo "=== Spec Coverage Report ==="
8echo "Spec: $SPEC_FILE"
9echo ""
10
11# Extract all AC numbers from spec
12ACS=$(grep -oP 'AC\d+' "$SPEC_FILE" | sort -u)
13ECS=$(grep -oP 'EC\d+' "$SPEC_FILE" | sort -u)
14
15COVERED=0
16TOTAL=0
17
18echo "--- Acceptance Criteria ---"
19for ac in $ACS; do
20 TOTAL=$((TOTAL + 1))
21 if grep -r "$ac" "$TEST_DIR" --include="*_test.go" -q; then
22 echo "✅ $ac: covered"
23 COVERED=$((COVERED + 1))
24 else
25 echo "❌ $ac: NOT covered in tests"
26 fi
27done
28
29echo ""
30echo "--- Edge Cases ---"
31for ec in $ECS; do
32 TOTAL=$((TOTAL + 1))
33 if grep -r "$ec" "$TEST_DIR" --include="*_test.go" -q; then
34 echo "✅ $ec: covered"
35 COVERED=$((COVERED + 1))
36 else
37 echo "❌ $ec: NOT covered in tests"
38 fi
39done
40
41echo ""
42echo "=== Spec Coverage: $COVERED/$TOTAL ($(( COVERED * 100 / TOTAL ))%) ==="Script ini bekerja dengan mengekstrak nomor AC/EC dari spec lalu men-grep-nya di file test, sehingga AC yang belum tersentuh test langsung tertandai. Hasilnya berupa laporan ringkas seperti berikut.
1=== Spec Coverage Report ===
2Spec: specs/order/cancel-order.md
3
4--- Acceptance Criteria ---
5✅ AC1: covered
6✅ AC2: covered
7...
8❌ AC10: NOT covered in tests ← drift risk!
9
10--- Edge Cases ---
11✅ EC1: covered
12❌ EC2: NOT covered in tests ← drift risk!
13
14=== Spec Coverage: 8/10 (80%) ===Angka 8/10 (80%) di akhir bukan sekadar metrik — ia bisa dijadikan gerbang: AC yang belum ter-cover adalah kandidat drift paling rawan karena tidak ada test yang menjaganya.
17.8 Deteksi via AI Periodic Review
Beberapa jenis drift terlalu semantik untuk ditangkap grep, dan di sinilah AI membantu. Prompt berikut menjadwalkan Claude Code melakukan spec compliance audit dengan output yang terstruktur.
1Tolong lakukan spec compliance audit untuk cancel order feature.
2
3SPEC (versi terbaru):
4@specs/order/cancel-order.md
5
6KODE SAAT INI:
7@internal/usecase/order/cancel_order.go, @internal/delivery/http/handler/order_handler.go, @internal/repository/postgres/order_repository.go
8
9Bandingkan spec dengan kode dan identifikasi:
101. AC/EC yang di-spec tapi tidak ada implementasinya di kode
112. Behavior di kode yang berbeda dari spec (response code, error code, etc.)
123. Field di response yang ada di kode tapi tidak di spec (atau sebaliknya)
134. Komentar di kode yang sudah outdated/salah dibanding spec
14
15Format output:
16## Spec Drift Report — [tanggal]
17### Ditemukan: [n] drift items
18### Items:
19| Type | Spec Says | Code Does | Location | Severity |AI unggul menangkap drift semantik yang tidak bisa dilihat pencocokan string — misalnya komentar yang sudah usang atau behavior yang secara logika menyimpang — asalkan findings-nya selalu diverifikasi manual karena AI bisa false positive.
17.9 Git Hook untuk Prevent Spec Drift
Deteksi terbaik adalah yang terjadi sebelum kode masuk repository. Pre-commit hook berikut memperingatkan developer ketika file handler berubah tanpa perubahan spec yang menyertainya.
1#!/bin/bash
2# .git/hooks/pre-commit
3
4# Detect if handler files changed without corresponding spec update
5CHANGED_HANDLERS=$(git diff --cached --name-only | grep "_handler.go")
6CHANGED_SPECS=$(git diff --cached --name-only | grep "^specs/")
7
8if [ -n "$CHANGED_HANDLERS" ] && [ -z "$CHANGED_SPECS" ]; then
9 echo "⚠️ WARNING: Handler files changed but no spec files updated."
10 echo ""
11 echo "Changed handlers:"
12 echo "$CHANGED_HANDLERS"
13 echo ""
14 echo "If this is a behavior change, please update the relevant spec."
15 echo "If this is a refactoring only, add a comment in your commit message:"
16 echo " 'refactor: no behavior change'"
17 echo ""
18 read -p "Continue anyway? (y/N) " -n 1 -r
19 echo
20 if [[ ! $REPLY =~ ^[Yy]$ ]]; then
21 exit 1
22 fi
23fiHook ini sengaja tidak memblokir total, hanya memaksa developer berhenti sejenak dan memutuskan sadar: apakah ini behavior change yang butuh update spec, atau refactoring murni? Gesekan kecil inilah yang mencegah drift menumpuk diam-diam.
17.10 Spec Drift dalam Review Checklist
Selain otomasi, drift check perlu masuk ke ritual review manusia. Bagian PR template berikut menjadikan pengecekan spec compliance sebagai langkah wajib, bukan opsional.
1## Pull Request Checklist
2
3### Spec Drift Prevention
4- [ ] Jika ada perubahan behavior: spec sudah di-update
5- [ ] Jika hanya refactoring: sudah di-confirm tidak ada behavior change
6- [ ] Error codes yang digunakan sesuai dengan yang ada di spec
7- [ ] HTTP status codes sesuai spec
8- [ ] Response format (field names, types) sesuai spec
9- [ ] Test names reference AC/EC dari spec
10
11### Spec Version
12- [ ] Implementasi berdasarkan spec versi: _______ (isi versi)
13- [ ] Jika spec di-update: changelog entry sudah ditambahkanChecklist ini mengubah “apakah kode sesuai spec?” dari pertanyaan yang mudah dilupakan menjadi item yang harus dicentang sebelum merge — memaksa author dan reviewer sama-sama memikirkan drift.
17.11 Recovering dari Spec Drift: Langkah Konkret
Ketika drift sudah terdeteksi, ada dua opsi pemulihan yang sah. Opsi pertama adalah memperbaiki kode agar kembali sesuai spec, seperti dirangkum langkah berikut.
1Kode saat ini:
2ErrorCode: "NOT_CANCELLABLE"
3
4Spec:
5AC9: error_code: "ORDER_NOT_CANCELLABLE"
6
7Action:
81. Fix kode → update error code di handler
92. Update test untuk assert exact string "ORDER_NOT_CANCELLABLE"
103. Check apakah ada consumer yang sudah depend pada "NOT_CANCELLABLE"
11 (jika ada, ini adalah breaking change yang perlu migration)
124. Commit: "fix(order): align error code with spec cancel-order.md AC9"Langkah ketiga adalah yang paling sering terlupakan: memperbaiki drift bisa jadi breaking change jika consumer sudah terlanjur bergantung pada nilai yang salah. Opsi kedua adalah sebaliknya — memperbarui spec agar sesuai kode, ketika perubahan kode memang keputusan sadar yang valid.
1Situasi: kode sengaja diubah berdasarkan diskusi yang valid tapi spec tidak diupdate.
2
3Action:
41. Update spec → AC9: error_code: "NOT_CANCELLABLE"
52. Update changelog: v1.4 - Changed error code format
63. Notify consumers yang mungkin depend pada old error code
74. Commit: "docs(spec): update cancel order error code per team decision 2025-07-15"Yang penting: kedua opsi menghasilkan spec dan kode yang kembali sinkron dan terdokumentasi — tidak pernah ada opsi ketiga “biarkan saja”.
17.12 Spec Drift Dashboard
Untuk tim yang lebih besar, hasil deteksi perlu diagregasi agar mudah dipantau. Program Go berikut adalah kerangka dashboard yang menghitung coverage dan drift item per spec.
1// tools/spec-dashboard/main.go
2
3package main
4
5import (
6 "fmt"
7 "os"
8 "path/filepath"
9 "regexp"
10 "strings"
11)
12
13type SpecCoverageResult struct {
14 SpecFile string
15 TotalACs int
16 CoveredACs int
17 TotalECs int
18 CoveredECs int
19 DriftItems []DriftItem
20}
21
22type DriftItem struct {
23 Type string // "missing_test", "behavior_mismatch"
24 AC string
25 Description string
26}
27
28func main() {
29 specs, _ := filepath.Glob("specs/**/*.md")
30
31 for _, spec := range specs {
32 result := analyzeSpec(spec)
33 printReport(result)
34 }
35}
36
37func analyzeSpec(specFile string) SpecCoverageResult {
38 content, _ := os.ReadFile(specFile)
39
40 // Extract AC/EC numbers
41 acPattern := regexp.MustCompile(`AC\d+`)
42 ecPattern := regexp.MustCompile(`EC\d+`)
43
44 acs := uniqueMatches(acPattern, string(content))
45 ecs := uniqueMatches(ecPattern, string(content))
46
47 // Check test coverage for each AC/EC
48 coveredACs := countCovered(acs)
49 coveredECs := countCovered(ecs)
50
51 return SpecCoverageResult{
52 SpecFile: specFile,
53 TotalACs: len(acs),
54 CoveredACs: coveredACs,
55 TotalECs: len(ecs),
56 CoveredECs: coveredECs,
57 }
58}Dengan mengiterasi seluruh folder specs/, dashboard ini memberi pandangan sekilas kesehatan spec compliance lintas domain — sangat berguna sebagai bahan diskusi di sprint retrospective.
17.13 Versioning Spec untuk Traceability
Deteksi drift hanya berarti jika kita tahu kode mengikuti spec versi berapa. Komentar Go berikut menanam referensi spec, versi, dan gap yang diketahui langsung di dekat implementasinya.
1// cancel_order.go
2
3// CancelOrderUseCase implements the cancel order business logic.
4// Spec: specs/order/cancel-order.md
5// Spec version: v1.3
6// Last compliance check: 2025-07-15
7//
8// Spec changes from previous version (v1.2 → v1.3):
9// - Added EC3: DB timeout handling
10// - Added NFR-S3: Rate limiting (implemented in middleware, not here)
11//
12// Known spec gaps (tracked):
13// - EC3 timeout: context deadline from HTTP middleware covers this (JIRA-789)
14func (uc *CancelOrderUseCase) Execute(ctx context.Context, input CancelOrderInput) error {Blok Known spec gaps adalah praktik jujur yang penting: ia mendokumentasikan kesenjangan yang diketahui beserta tiket pelacaknya, sehingga gap yang disengaja tidak salah dikira drift oleh reviewer atau AI berikutnya.
17.14 Mengintegrasikan Spec Drift Detection ke Sprint
Deteksi drift paling efektif ketika menjadi bagian ritual sprint, bukan aktivitas terpisah. Rangkuman berikut menempatkan pengecekan spec di setiap upacara sprint.
1Sprint Ceremonies dengan Spec Drift Component:
2
3Sprint Planning:
4- Review spec untuk fitur yang akan di-implementasi
5- Verifikasi spec versi yang digunakan adalah yang terbaru
6
7Mid-Sprint Check-in:
8- Quick AI spec review untuk fitur yang sedang in-progress
9- Flag jika ada yang keluar dari spec
10
11Sprint Review:
12- Demo fitur → cek apakah demo sesuai dengan spec
13- Spot-check error codes dan response format
14
15Sprint Retrospective:
16- "Apakah ada spec drift yang ditemukan sprint ini?"
17- "Apa yang menyebabkannya?"
18- "Bagaimana kita bisa prevent di sprint berikutnya?"Dengan menyebar pengecekan ke seluruh siklus, drift ditangkap sedini mungkin — bukan menumpuk sampai jadi kejutan besar di akhir sprint.
17.15 Tanda-Tanda Spec Drift yang Perlu Diwaspadai
Saat code review, tidak semua penyimpangan sama berbahayanya. Klasifikasi risiko berikut membantu reviewer memfokuskan perhatian pada yang paling berdampak.
1🔴 HIGH RISK:
2- Error code yang berbeda dari spec (consumers akan break)
3- HTTP status code yang berbeda dari spec
4- Field yang dihilangkan dari response
5- Validation yang di-skip
6
7🟡 MEDIUM RISK:
8- Field yang ditambah ke response (biasanya backward-compatible tapi perlu verify)
9- Timeout atau limit yang berbeda dari NFR
10- Logging yang tidak sesuai NFR observability
11
12🟢 LOW RISK:
13- Komentar yang outdated
14- Variable names yang tidak sesuai convention di spec
15- Error message text yang sedikit berbeda (user-facing, tapi tidak di-machine-parsed)Skala warna ini memberi bahasa bersama untuk memprioritaskan: drift HIGH RISK wajib diperbaiki sebelum rilis, sedangkan LOW RISK bisa dijadwalkan tanpa memblokir delivery.
17.16 Spec Drift di Environment CI/CD
Semua teknik deteksi di atas paling bertaji ketika dijalankan otomatis di pipeline. Workflow berikut merangkai spec coverage, contract validation, dan linter menjadi satu gerbang CI.
1# .github/workflows/spec-compliance.yml
2
3name: Spec Compliance Check
4
5on:
6 pull_request:
7 paths:
8 - 'internal/**'
9 - 'specs/**'
10
11jobs:
12 spec-compliance:
13 steps:
14 - name: Run spec coverage check
15 run: |
16 bash scripts/spec-coverage.sh specs/order/cancel-order.md
17 bash scripts/spec-coverage.sh specs/order/create-order.md
18 # Add more specs as needed
19
20 - name: Run OpenAPI contract validation
21 run: |
22 go test ./internal/delivery/http/... -run TestContract -v
23
24 - name: Check error code registry
25 run: |
26 golangci-lint run --enable=errorcodecheck ./internal/delivery/...
27
28 - name: AI Spec Review (optional, manual trigger)
29 if: github.event.inputs.ai_review == 'true'
30 run: |
31 # Trigger AI spec review via Claude API
32 # (implementation depends on CI setup)
33 echo "AI spec review triggered"Filter paths memastikan pengecekan hanya berjalan saat kode atau spec berubah, sehingga gerbang ini tetap cepat namun tidak pernah membiarkan perubahan berisiko lewat tanpa diperiksa.
17.17 Penanganan Spec Drift yang Sudah Lama
Kadang tim baru menyadari drift setelah berbulan-bulan berjalan, dan ini butuh penanganan khusus. Prompt berikut meminta Claude Code menyusun audit lengkap plus rencana pemulihan yang tidak merusak consumer.
1Kami baru menyadari bahwa cancel order endpoint sudah drift dari spec
2selama 3 bulan. Beberapa perubahan yang tidak terdokumentasi:
31. Error code berubah
42. Response body untuk 204 sekarang ada body (spec bilang no content)
53. Timeout window 15 menit di kode, spec bilang 30 menit
6
7Bantu saya membuat:
81. Audit report yang lengkap dari semua drift
92. Prioritas fix (mana yang harus fix dulu?)
103. Plan untuk recover tanpa breaking existing consumers
114. Cara update spec yang akurat untuk mendokumentasikan current behavior
12 sebagai "v2.0" sambil keep v1.3 sebagai historical recordKunci penanganan drift lama adalah realisme: begitu perilaku “salah” sudah dipakai consumer di production, memperbaikinya menjadi breaking change — jadi rencana harus menyertakan strategi migrasi dan versioning, bukan sekadar “perbaiki sekarang”.
17.18 Tips & Gotchas
💡 Tip 1: Test yang assert exact string values adalah best drift detector
Test yang assert assert.Equal(t, "ORDER_NOT_CANCELLABLE", errorCode) akan fail langsung ketika ada drift. Test yang hanya check status == 409 tidak akan detect error code drift.
💡 Tip 2: Spec version di komentar kode adalah investasi jangka panjang
// Spec: cancel-order.md v1.3 di kode membantu developer masa depan untuk tahu spec mana yang diikuti dan apakah ada update.
💡 Tip 3: Spec coverage report sebagai definisi “done”
Feature belum “done” sampai spec coverage ≥ 80% dan semua AC/EC yang high-priority punya test.
💡 Tip 4: Communicate drift sebagai “good catch” bukan blame
Spec drift biasanya terjadi karena proses, bukan karena developer ceroboh. Temukan drift → perbaiki → improve proses → bukan temukan drift → cari siapa yang salah.
⚠️ Gotcha 1: Drift detection yang terlalu agresif bisa jadi bottleneck
Jika setiap tiny change butuh spec update, developer akan bypass spec process. Find the balance.
⚠️ Gotcha 2: AI drift detection bisa false positive
AI mungkin bilang “AC ini tidak ter-implement” padahal implementasinya ada tapi di file yang berbeda atau via middleware. Selalu verify findings.
⚠️ Gotcha 3: Spec drift berbeda dari spec evolution
Spec yang sengaja di-update dengan keputusan tim bukan drift — itu evolusi. Drift adalah perubahan yang tidak disadari atau tidak terdokumentasi.
⚠️ Gotcha 4: Consumer notification wajib saat fix drift yang breaking
Jika drift sudah di-production dan consumers sudah depend pada perilaku yang “salah”, memperbaiki drift menjadi breaking change. Komunikasikan sebelum deploy.
17.19 Membangun Spec Drift Prevention Culture
Tools dan process tidak cukup — prevention membutuhkan culture. Empat momen percakapan berikut adalah tempat culture itu dibangun sehari-hari.
Dalam daily standup: “Apakah ada yang berencana mengubah behavior endpoint yang sudah ada hari ini?”
Dalam code review: “Perubahan ini mengubah error code. Apakah spec sudah di-update?”
Dalam retrospective: “Apakah ada spec drift yang kita temukan sprint ini? Apa root cause-nya?”
Dalam onboarding developer baru: “Setiap behavior change butuh spec update. Ini adalah non-negotiable di tim kita.”
Ketika pertanyaan-pertanyaan ini menjadi natural dalam percakapan tim, spec drift akan semakin jarang terjadi — bukan karena tools yang lebih ketat, tapi karena kesadaran kolektif yang sudah tertanam.
17.20 Ringkasan
Spec drift adalah musuh SDD yang paling berbahaya karena sifatnya yang silent dan incremental. Pencegahan lebih baik dari deteksi.
Pencegahan terbaik: Test yang explicitly assert string values dari spec (error codes, field names) — ini adalah “alarm system” yang otomatis berbunyi ketika ada drift.
Deteksi berkala: Spec coverage report, periodic AI audit, dan OpenAPI contract test memberikan lapisan deteksi yang complementary.
Culture > Tools: Git hooks, CI checks, dan custom linter membantu, tapi yang paling efektif adalah tim yang sadar pentingnya spec compliance dan dengan natural menanyakan “apakah ini sesuai spec?” dalam setiap code review.
Recovery protocol: Drift yang ditemukan harus di-address dengan satu dari dua pilihan — fix kode ke spec atau update spec sesuai kode (dengan dokumentasi keputusan). Tidak ada opsi ketiga “biarkan saja”.
Di artikel berikutnya, kita bahas SDD dalam konteks tim yang lebih besar: Versioning Spec, Onboarding Developer Baru, dan Code Review dalam ekosistem SDD yang matang.