Skip to content

archit-io/DecisionOps-ADR

Repository files navigation

DecisionOps

ADR (Architecture Decision Record) governance system with immutable versioning, FSM-based status enforcement, and compliance-grade audit trail.

Problem Statement

Architecture decisions at scale require governance, not CRUD. Teams make decisions, revisit them months later when context has degraded, and struggle with questions like: What alternatives were considered? Why was this chosen? Is this still valid? Markdown files in Git fail to enforce structure, transitions, or periodic review. DecisionOps formalizes ADR lifecycle management as a first-class concern with enforced quality gates, explicit status transitions, and time-based review triggers.

Documentation

Core Concepts

ADR vs ADR Version

An ADR is a stable container with mutable status (DRAFT → PROPOSED → ACCEPTED → DEPRECATED). Edits do not mutate the ADR — instead, a new AdrVersion is created with an incremented version number. The ADR points to its currentVersion, preserving full edit history. This separation ensures immutability of decisions while allowing refinement.

Status Lifecycle (FSM)

Transitions are enforced via finite state machine:

DRAFT → PROPOSED
PROPOSED → DRAFT | ACCEPTED
ACCEPTED → DEPRECATED
DEPRECATED → (terminal)

Invalid transitions (e.g., DRAFT → DEPRECATED) are rejected at the service layer. No database triggers — enforcement is explicit.

Versioning & Immutability

Every edit operation (updateAdr) creates a new AdrVersion row. Previous versions remain queryable. This enables:

  • Full decision evolution history
  • Time-travel queries ("What did v3 say?")
  • Regulatory audit compliance (immutable trail)

Alternatives, tags, and links are versioned alongside the decision content.

Audit Trail

Every state transition emits an AuditEvent:

  • CREATED — ADR initialized
  • VERSION_CREATED — New version added
  • STATUS_CHANGED — Lifecycle transition
  • SNOOZED — Review deferred

Events capture actorId, timestamp, and payload. Indexed for chronological queries.

Review Cadence

When an ADR transitions to ACCEPTED, a reviewDate must be set. A background query surfaces ADRs due for review. Users can snooze reviews with a reason, extending the due date. This enforces periodic revisitation of accepted decisions.

Key Features

  • FSM-enforced status transitions — No invalid state changes
  • Immutable version history — All edits create new versions
  • Quality gates on acceptance — Minimum 2 alternatives, non-trivial trade-offs, substantive decision text
  • Review-due engine — Time-based re-evaluation triggers
  • Full-text search (Postgres FTS)tsvector + GIN index for title/summary/tag queries
  • Compliance-grade audit log — Immutable event stream per ADR
  • Snooze mechanism — Defer review with reason + actor tracking
  • Versioned alternatives — Pros/cons/complexity/reversibility scores per alternative, per version

Architecture

Tech Stack

  • Next.js 16 (App Router)
  • TypeScript (strict mode)
  • Prisma 6.2 + PostgreSQL
  • Zod validation
  • Vitest (unit + API integration tests)

Domain Model

Adr (container)
  ├─ status: AdrStatus
  ├─ reviewDate: DateTime?
  ├─ currentVersionId → AdrVersion
  └─ versions: AdrVersion[]

AdrVersion (immutable content)
  ├─ versionNumber: Int
  ├─ title, summary, context, decision, tradeOffs
  ├─ tags: String[]
  ├─ links: Json
  └─ alternatives: Alternative[]

Alternative
  ├─ name
  ├─ pros[], cons[]
  └─ complexity, reversibility (1-5 scale)

AuditEvent
  ├─ type: AuditEventType
  ├─ actorId, payload, createdAt
  └─ indexed by [adrId, createdAt DESC]

AdrSearch (Postgres FTS)
  ├─ adrId (PK)
  ├─ document: tsvector (generated from title + summary + tags)
  └─ GIN index for tsquery matching

Service Layer Enforcement

src/lib/adr/service.ts is the single source of truth for mutations. API routes delegate to service functions:

  • createAdr() — Initialize with version 1, emit CREATED + VERSION_CREATED events
  • updateAdr() — Create new version, update currentVersionId, emit VERSION_CREATED
  • changeStatus() — Validate FSM transition, run quality gates if accepting, emit STATUS_CHANGED
  • snoozeAdr() — Extend reviewDate, emit SNOOZED
  • getAdrsDueForReview() — Query accepted ADRs with reviewDate <= now()

All mutations run in Prisma transactions. Partial failure rolls back entirely.

Quality Gates

When transitioning to ACCEPTED, the system runs acceptance gates:

  • Decision must be substantive (≥10 chars, non-trivial)
  • Trade-offs must include concrete statements (≥20 chars, contains punctuation)
  • Minimum 2 alternatives required
  • Each alternative must have ≥1 pro, ≥1 con, complexity/reversibility scores

Gates fail the transaction if unsatisfied. Errors include field paths for client display.

API Boundaries

REST-like routes under /api/adr:

  • POST /api/adr — Create ADR with initial version
  • GET /api/adr/list — Paginated list with filters (status, tags, search)
  • GET /api/adr/[id] — Fetch ADR with current version + alternatives
  • PATCH /api/adr/[id] — Update ADR (creates new version)
  • PATCH /api/adr/[id]/status — Transition status (FSM validation)
  • GET /api/adr/review-due — ADRs requiring review
  • PATCH /api/adr/[id]/snooze — Defer review
  • GET /api/adr/search?q=... — Full-text search via tsquery

All inputs validated with Zod. Errors return structured JSON with problem details.

Decision Lifecycle

1. Create (DRAFT)
   ├─ Initial version created
   └─ Audit: CREATED, VERSION_CREATED

2. Propose
   ├─ Transition DRAFT → PROPOSED
   └─ Audit: STATUS_CHANGED

3. Accept
   ├─ Quality gates enforced
   ├─ reviewDate required
   ├─ Transition PROPOSED → ACCEPTED
   └─ Audit: STATUS_CHANGED

4. Edit (creates new version)
   ├─ Version number incremented
   ├─ currentVersionId updated
   └─ Audit: VERSION_CREATED

5. Review
   ├─ Surfaced when reviewDate <= now()
   ├─ Can snooze with reason
   └─ Audit: SNOOZED

6. Deprecate
   ├─ Transition ACCEPTED → DEPRECATED
   └─ Audit: STATUS_CHANGED (terminal)

Editing Rules

Content edits are allowed only in DRAFT or PROPOSED status. These edits create new versions with incremented versionNumber, updating the ADR's currentVersionId.

ACCEPTED ADRs are immutable. No further edits are permitted. If an accepted decision requires revision, create a new ADR that supersedes the original. The original remains in the system as historical record.

This design prioritizes auditability over convenience. Regulatory/compliance contexts require "what was the decision at time T?" to be answerable definitively.

Search & Discoverability

Postgres full-text search via AdrSearch materialized view:

  • document column is a tsvector computed from title + summary + tags
  • GIN index accelerates tsquery matching
  • Upserted on every version creation
  • Search query: SELECT * FROM "AdrSearch" WHERE document @@ to_tsquery('english', $1)

Client sends plain-text query; backend converts to tsquery with basic stemming/ranking.

Deployment

Public demo: Intentionally deployed without authentication on Railway. This is a demonstration system, not production-ready. Authentication (e.g., Clerk, Auth.js) is a known gap.

Environment variables:

DATABASE_URL=postgresql://...

Migrations:

npx prisma migrate deploy
node scripts/postmigrate.js  # Creates FTS indexes (not in Prisma schema)

FTS indexes defined in prisma/sql/fts_indexes.sql, applied post-migration.

Tradeoffs & Non-Goals

Intentional Limitations

  • No authentication — Demo system. Add Clerk/Auth.js for real use.
  • No RBACactorId is tracked but not enforced. Add role-based permissions for multi-tenant.
  • No deletion — ADRs are deprecated, not deleted. Terminal DEPRECATED status preserves full history.
  • No version diff UI — Versions are stored but not visualized side-by-side.
  • No approval workflow — Status changes are unilateral. Add approval chains for hierarchical orgs.
  • No notifications — Review-due ADRs require manual polling. Add email/Slack webhooks.

Rejected Approaches

  • Markdown files in Git — No structured queries, no enforced schema, no lifecycle management.
  • Event sourcing — Overkill for this domain. Append-only audit log suffices.
  • GraphQL — REST is sufficient; GraphQL adds complexity without clear ROI.
  • NoSQL — Relational model enforces referential integrity; Postgres FTS is adequate.

Running Locally

# Install dependencies
npm install

# Setup Postgres (Docker example)
docker run -d -p 5432:5432 \
  -e POSTGRES_PASSWORD=postgres \
  -e POSTGRES_DB=decisionops \
  postgres:16-alpine

# Configure environment
echo "DATABASE_URL=postgresql://postgres:postgres@localhost:5432/decisionops" > .env

# Run migrations + FTS setup
npx prisma migrate deploy
node scripts/postmigrate.js

# Start dev server
npm run dev

Navigate to http://localhost:3000. Create an ADR, transition it through statuses, observe version history and audit events.

Tests

# Unit + integration tests
npm test

# Coverage report
npm run test:coverage

Tests mock Prisma client. See tests/mocks/prisma.ts for factory usage.

About

DecisionOps — Engineering Decision Governance System ADR lifecycle management with FSM-enforced status transitions, immutable versioning, quality gates, audit trail, revisit engine, and Postgres full-text search. Built to demonstrate governance-first system design, not CRUD.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages