Initial commit
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
# Bounded Context Architecture Statement
|
||||
|
||||
This repository treats **bounded contexts as first-class language boundaries**.
|
||||
They should be identified early enough to shape naming, ownership, review, and code placement, but they are **not permanent**.
|
||||
A bounded context is a provisional domain boundary that may be split, merged, or reshaped later when real change pressure shows the current boundary is wrong.
|
||||
|
||||
The goal is to combine:
|
||||
|
||||
- the clarity of Domain-Driven Design language boundaries
|
||||
- the speed of feature-by-feature implementation
|
||||
- the reviewability benefits of smaller bounded seams
|
||||
- the safety of continuous refactoring over waterfall lock-in
|
||||
|
||||
## Core position
|
||||
|
||||
### 1. Bounded contexts are planned early, not frozen early
|
||||
|
||||
This repo does **not** treat bounded contexts as something that should be discovered only after a large amount of code exists.
|
||||
It is usually worth defining an initial bounded-context map early because it gives:
|
||||
|
||||
- a stable language boundary
|
||||
- a first-pass ownership model
|
||||
- an initial code organization strategy
|
||||
- a clearer review surface for human and AI contributors
|
||||
|
||||
However, this initial map is only a starting point.
|
||||
It should be treated as a hypothesis that is refined as the system evolves.
|
||||
|
||||
### 2. Cross-context business processes are modeled as top-level workflows
|
||||
|
||||
A business process such as checkout may involve multiple bounded contexts such as inventory, payment, and receipts.
|
||||
That does **not** mean those contexts should call each other directly.
|
||||
|
||||
Instead:
|
||||
|
||||
- cross-context coordination belongs in top-level workflows
|
||||
- workflows may orchestrate multiple contexts
|
||||
- each context keeps its own internal policies, models, and implementation details local
|
||||
|
||||
This makes the workflow graph the place where cross-context business processes are visible and reviewable.
|
||||
|
||||
### 3. Contexts do not talk to each other directly
|
||||
|
||||
A bounded context should not directly import another bounded context's internal files or make decisions inside another context's language.
|
||||
If one context needs something from another, that interaction should happen through a top-level workflow that calls each context through its public API.
|
||||
|
||||
The intended rule is:
|
||||
|
||||
> Cross-context access goes through public intent APIs via top-level workflows.
|
||||
|
||||
This keeps context ownership clear and prevents hidden coupling between contexts.
|
||||
|
||||
### 4. Internal seams are smaller than bounded contexts
|
||||
|
||||
A bounded context is not the smallest review unit.
|
||||
Inside a bounded context there may still be smaller seams such as:
|
||||
|
||||
- public intent APIs
|
||||
- workflow steps
|
||||
- policies
|
||||
- translators
|
||||
- adapters
|
||||
- module entrypoints
|
||||
|
||||
These seams exist to keep implementation reviewable and refactorable.
|
||||
A bounded context gives the outer language boundary.
|
||||
Internal seams give local structure inside that boundary.
|
||||
|
||||
## Repository structure implications
|
||||
|
||||
The primary organization should be **by context**, not by global layer.
|
||||
Top-level folders should stay small and reserved for true cross-context concerns.
|
||||
|
||||
A typical direction is:
|
||||
|
||||
```text
|
||||
src/
|
||||
├── shared/ # tiny ubiquitous primitives only
|
||||
├── workflows/ # top-level cross-context orchestration only
|
||||
├── tasks/ # optional cross-context workflow steps, only if needed
|
||||
└── contexts/ # first-class bounded contexts
|
||||
```
|
||||
|
||||
Within each context, the context may contain some combination of:
|
||||
|
||||
- domain models
|
||||
- policies
|
||||
- workflows or tasks
|
||||
- layer definitions
|
||||
- adapters
|
||||
- registries
|
||||
- translators
|
||||
- lib
|
||||
|
||||
But these should be treated as an **allowed menu**, not a mandatory checklist.
|
||||
Do not create internal folders just to satisfy symmetry.
|
||||
Only create the internal seams that the current context actually needs.
|
||||
|
||||
## Shared types and translation
|
||||
|
||||
The repository should avoid sharing **rich domain types** across bounded contexts.
|
||||
A type that looks generic often becomes a hidden coupling point if different contexts need different meaning, rules, or fields.
|
||||
|
||||
Examples:
|
||||
|
||||
- a shipping address may not be the same concept as a profile address
|
||||
- a payment identifier may not mean the same thing as an accounting identifier
|
||||
- an inventory quantity may not carry the same rules as a purchasing quantity
|
||||
|
||||
So the rule is:
|
||||
|
||||
- keep `shared/` small
|
||||
- use it only for truly ubiquitous primitives and low-meaning technical building blocks
|
||||
- keep rich domain models local to their context
|
||||
- translate at the boundary when data moves between contexts
|
||||
|
||||
This follows the DDD idea that context edges are where translation belongs.
|
||||
The purpose of translation is not ceremony for its own sake.
|
||||
It is to stop one context's language from silently taking over another.
|
||||
|
||||
## Policies at the top level
|
||||
|
||||
This repository should **not** start with a top-level `policies/` folder by default.
|
||||
Most policy logic belongs inside a bounded context because most business rules are written in the language of one context.
|
||||
|
||||
For cross-context workflows:
|
||||
|
||||
- start with orchestration logic in the workflow
|
||||
- keep cross-context policy inline until it becomes a real stable seam
|
||||
- only extract a top-level policy when the rule is truly about the relationship between contexts or the process as a whole
|
||||
|
||||
This avoids blessing a global policy layer before there is evidence that it is needed.
|
||||
|
||||
## Folders before packages
|
||||
|
||||
Bounded contexts should usually start as **folders/modules in one package**, not as separate packages.
|
||||
|
||||
Why folders first:
|
||||
|
||||
- lower tooling overhead
|
||||
- easier to reshape while boundaries are still provisional
|
||||
- simpler imports and refactors during early design pressure
|
||||
- enough structure to enforce public APIs and context ownership
|
||||
|
||||
Why not packages immediately:
|
||||
|
||||
- package boundaries are harder to change
|
||||
- they add build and tooling complexity early
|
||||
- they can create premature rigidity before the domain map has earned it
|
||||
|
||||
Packages may make sense later if a context becomes operationally independent enough to justify stronger enforcement.
|
||||
But the default should be folders first, packages later if proven necessary.
|
||||
|
||||
## Refactoring stance
|
||||
|
||||
Refactoring is part of the architecture, not a repair step after the fact.
|
||||
This repo should assume that:
|
||||
|
||||
- some initial bounded contexts will be wrong
|
||||
- some contexts will need to split
|
||||
- some initially separate contexts will later merge
|
||||
- internal seams will evolve under real use
|
||||
|
||||
That is not a failure of DDD.
|
||||
It is how a domain model becomes more accurate over time.
|
||||
|
||||
The important thing is to preserve a stable review model while allowing structure to improve.
|
||||
The combination used here is:
|
||||
|
||||
- define provisional bounded contexts early
|
||||
- keep cross-context coordination at top-level workflows
|
||||
- keep rich models inside contexts
|
||||
- prefer public APIs at boundaries
|
||||
- refine seams and boundaries through refactoring when change pressure justifies it
|
||||
|
||||
## Lightweight principles
|
||||
|
||||
Use these as the short version:
|
||||
|
||||
1. Bounded contexts are first-class language boundaries.
|
||||
2. Bounded contexts are planned early but treated as provisional.
|
||||
3. Only top-level workflows coordinate across contexts.
|
||||
4. Contexts do not call each other directly.
|
||||
5. Cross-context access goes through public intent APIs via top-level workflows.
|
||||
6. `shared/` is tiny and contains only truly ubiquitous primitives.
|
||||
7. Rich domain models stay local to their context.
|
||||
8. Translation happens at context edges when data crosses boundaries.
|
||||
9. Internal seams are smaller than bounded contexts and should emerge under real pressure.
|
||||
10. Split or merge bounded contexts based on change pressure and reviewability, not theory alone.
|
||||
|
||||
## What this architecture is optimizing for
|
||||
|
||||
This shape is intended to optimize for:
|
||||
|
||||
- strong domain language boundaries
|
||||
- low hidden coupling
|
||||
- easier human review of AI-generated code
|
||||
- safer localized refactoring
|
||||
- incremental delivery without pretending the first domain map is final
|
||||
@@ -0,0 +1,26 @@
|
||||
# Composition over Events
|
||||
|
||||
## The Decision
|
||||
We prioritize **Direct Function Composition** (via `pipe`) for core business logic. We use **Events** only for side effects that do not impact the transaction's success.
|
||||
|
||||
## The Reasoning
|
||||
|
||||
### 1. The "Microservice Envy" Trap
|
||||
Developers often break monoliths into event-driven fragments to "decouple" them. Inside a single application, this leads to:
|
||||
- **Loss of Observability**: You can't trace a request top-to-bottom.
|
||||
- **Error Handling Complexity**: If the event consumer fails, the producer doesn't know.
|
||||
- **Refactoring Pain**: "Find Usages" breaks.
|
||||
|
||||
### 2. Local Reasoning
|
||||
With Effect, `step1.pipe(step2)` provides:
|
||||
- **Transactional Integrity**: If step2 fails, the whole block fails (or recovers).
|
||||
- **Type Safety**: The output of step1 is typed as the input of step2.
|
||||
- **Readability**: The linear flow is visible.
|
||||
|
||||
### 3. When to use Events
|
||||
Use events for **Fire-and-Forget** side effects:
|
||||
- Sending analytics
|
||||
- Sending welcome emails
|
||||
- Notifying external webhooks
|
||||
|
||||
If the business outcome depends on it (e.g., "Charge Card"), it must be a function call, not an event.
|
||||
@@ -0,0 +1,65 @@
|
||||
# History & Architectural Decisions
|
||||
|
||||
This document tracks the evolution of our architectural decisions, including the paths we considered and ultimately rejected.
|
||||
|
||||
## 1. The "3-Layer" vs "Granular Seams" Debate
|
||||
|
||||
**The Consideration:**
|
||||
Should we stick to the industry-standard 3-Layer architecture (Controller -> Service -> Repository) for simplicity?
|
||||
|
||||
**The Counter-Argument (Why we rejected 3-Layer):**
|
||||
- **The "Fat Service" Problem:** In 3-layer systems, the "Service" layer becomes a magnet for *everything*—business logic, validation, I/O, and orchestration.
|
||||
- **Testing Difficulty:** To test a business rule in a Service, you often have to mock the Repository, because they are tightly coupled.
|
||||
- **Effect Granularity:** In Effect, seams are cheap. There is no performance penalty for splitting decisions from mechanics.
|
||||
|
||||
**The First Decision:**
|
||||
We initially described the repo as an **Onion Architecture** with granular layers:
|
||||
1. Domain Models (Pure Logic)
|
||||
2. Policies (Pure Decisions)
|
||||
3. Workflows (Impure Orchestration)
|
||||
4. Infrastructure (Impure I/O)
|
||||
|
||||
**The Later Reframe:**
|
||||
We kept the purity-based seam logic, but stopped treating the global layer tree as the main repository map.
|
||||
The repo now prefers **bounded contexts first**, with those smaller seams living mostly *inside each context*.
|
||||
|
||||
## 2. The "Event-Driven Everything" Debate
|
||||
|
||||
**The Consideration:**
|
||||
Should we use Effect's Event system (Pub/Sub) to decouple workflows? (e.g., `Workflow A` emits `Event X`, `Workflow B` listens).
|
||||
|
||||
**The Counter-Argument (Why we rejected internal events):**
|
||||
- **Loss of Locality:** You cannot click "Go to Definition" to see what happens next. The control flow becomes invisible.
|
||||
- **Error Propagation:** If the listener fails, the emitter doesn't know. You lose the transactional integrity of `Effect.gen`/`pipe`.
|
||||
- **Complexity:** It requires an internal event bus infrastructure that mimics microservices but inside a monolith ("Microservice Envy").
|
||||
|
||||
**The Decision:**
|
||||
Use **Direct Composition** (Functions calling Functions) for all core business logic.
|
||||
Use **Events** ONLY for "fire-and-forget" side effects (e.g., sending analytics, welcome emails) where failure does not invalidate the transaction.
|
||||
|
||||
## 3. The naming of adapters
|
||||
|
||||
**The Consideration:**
|
||||
Should we name adapters by their implementation (e.g., `StripeAdapter`) or their role (e.g., `SmallPaymentService`)?
|
||||
|
||||
**The Counter-Argument:**
|
||||
- **Implementation Names (`StripeAdapter`)**: Couples the domain to the vendor. If we switch to PayPal, we have to rename everything.
|
||||
- **Role Names (`SmallPaymentService`)**: Can drift semantically. What if "Small" becomes "VIP"? The name becomes a lie.
|
||||
|
||||
**The Decision:**
|
||||
Use **Strategy Pattern**:
|
||||
- **Interface**: `PaymentInterface` (Generic).
|
||||
- **Policy**: Returns a Strategy Value (e.g., `RETAIL_CHANNEL`).
|
||||
- **Registry**: Maps `RETAIL_CHANNEL` -> `StripeAdapter`.
|
||||
- **Workflow**: Asks Registry for the implementation.
|
||||
|
||||
## 4. The "Checks" Layer
|
||||
|
||||
**The Consideration:**
|
||||
Should we have a top-level `checks/` folder for simple predicates?
|
||||
|
||||
**The Rejection:**
|
||||
It encouraged "Anemic Domain Models" where logic was stripped away from the data types.
|
||||
|
||||
**The Decision:**
|
||||
Move "Checks" into the **Domain Model** modules (`src/domain/models/`). Logic about an entity belongs *with* the entity schema.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Architecture Reasoning
|
||||
|
||||
This folder contains deep-dive explanations for the architectural decisions made in this template.
|
||||
The current governing direction is **context-first architecture**: bounded contexts are the main language boundary, while purity and orchestration rules still shape the seams inside and between contexts.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Bounded Context Architecture Statement](bounded-contexts.md): The current architecture baseline for code placement, seams, and cross-context coordination.
|
||||
- [Architecture Rationale Summary](rationale-summary.md): The short version for review refreshers and interviews.
|
||||
- [Layers & Granularity](layers-granularity.md): How purity-based seams still matter once organization becomes context-first.
|
||||
- [Pure Logic vs Orchestration](pure-logic.md): The separation of decisions from orchestration inside contexts and top-level workflows.
|
||||
- [Service Strategy](service-strategy.md): How public APIs, adapters, and optional registries fit inside bounded contexts.
|
||||
- [Composition over Events](composition.md): Why we prefer direct function composition over internal event buses for core logic.
|
||||
- [Type-Driven Design](type-driven-design.md): Encoding business rules into the type system ("Make Illegal States Unrepresentable").
|
||||
- [History & Decisions](history.md): A log of architectural alternatives considered, adopted, or later reframed.
|
||||
- [Security Verification Rationale](security-verification-rationale.md): Why security review is modeled as verification gates, why there are separate design and implementation skills, and why sink handling and capability scope are emphasized.
|
||||
@@ -0,0 +1,30 @@
|
||||
### Core String Security & AI Architecture Principles
|
||||
|
||||
#### 1. Type-Level Security (Data Provenance & Taint Tracking)
|
||||
* **Never pass raw strings:** A raw string has no memory. Do not pass raw `string` types to sensitive sinks (database queries, AI contexts, DOM rendering).
|
||||
* **Enforce wrapper objects:** Use strongly typed objects (e.g., `TrustedInput`, `TaintedInput`) to carry the string.
|
||||
* **Track provenance:** The type must explicitly state where the string came from (its source) and whether it is trusted or tainted. *Enforce this at the compiler/type-checker level if your language allows it (e.g., TypeScript, Rust).*
|
||||
|
||||
#### 2. Defense in Depth (Ingestion vs. Use)
|
||||
* **Filter strictly at Ingestion:** Reject bad data early. Enforce maximum lengths, validate file extensions/magic numbers, check data types, and drop edge cases (like null bytes). This keeps the system state clean.
|
||||
* **Sanitize/Encode AT the Point of Use:** Remember that "dangerous" is context-dependent. Apply specific encoding right before the string is consumed (e.g., HTML escaping for web renders, Parameterized Binding for SQL, strict XML encapsulation for LLMs).
|
||||
|
||||
#### 3. Structural Boundaries (No Concatenation)
|
||||
* **Parameterized Queries & Messages:** Never concatenate strings to build a query or an AI prompt. For databases, rely on the ORM or parameterized query objects. For AI, use API message arrays (e.g., `[{role: "system", content: "..."}, {role: "user", content: "..."}]`) to structuralize the conversation.
|
||||
|
||||
#### 4. The Data Abstraction (Control vs. Data Planes)
|
||||
* **Isolate Sub-Agents:** When handling untrusted data, pass the data to an isolated "Worker" AI that has zero access to tools or APIs.
|
||||
* **Shield the Orchestrator:** Strive to return the Worker AI's output directly to the user. Avoid sending the tainted output back into the context window of your main "Boss/Orchestrator" AI.
|
||||
|
||||
#### 5. Mitigation for Orchestrator Taint (If you *must* feed it back)
|
||||
* **Principle of Least Privilege:** If the orchestrator *must* read a tainted summary, restrict its available tools to the absolute minimum necessary for that specific turn of conversation.
|
||||
* **Human-in-the-Loop (HITL):** Any destructive or sensitive command invoked by an AI (e.g., dropping databases, sending emails, transferring funds) must pause execution and require explicit human authorization.
|
||||
* *(New)* **Explicit Delimiters:** When injecting untrusted text into a prompt, lock it inside clear, hard-to-guess delineators (e.g., `<user_document_untrusted> [DATA] </user_document_untrusted>`) so the model knows where the data starts and stops.
|
||||
|
||||
|
||||
#### 6. AI Guardrails (Pre-computation Filtering)
|
||||
* Before sending user input to a slow, expensive AI agent, pass it through a tiny, fast classification model (like Llama Guard) designed solely to detect prompt injection or jailbreak attempts. If it flags as malicious, block the request before it even reaches your main orchestration logic.
|
||||
|
||||
#### 7. Audit Logging
|
||||
* Because AI agents are non-deterministic, you must log all tool invocations and the exact context (the prompts and tainted strings) that led to that tool being called. If a prompt injection attack *does* succeed, you need this log to figure out how they tricked the agent.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Layers & Granularity After the Bounded-Context Shift
|
||||
|
||||
## The Decision
|
||||
We no longer treat a global N-layer onion as the primary repository map.
|
||||
Instead, we organize the repo by **bounded context first**, while still using **purity-based seams** inside each context and across top-level workflows.
|
||||
|
||||
## The Reasoning
|
||||
|
||||
### 1. The trap of the global service layer still exists
|
||||
In traditional 3-layer architectures, the "Service" layer becomes a dumping ground. It often handles:
|
||||
- Validation
|
||||
- Business Rules
|
||||
- Orchestration
|
||||
- I/O
|
||||
- Data Transformation
|
||||
|
||||
That problem still matters.
|
||||
The fix is still to separate concerns by purity and intent.
|
||||
What changed is **where that separation is expressed**: mostly inside each context, not as one global folder tree.
|
||||
|
||||
### 2. Context-first, seams-second
|
||||
In this template, bounded contexts give the outer language boundary.
|
||||
Inside those boundaries, we still use smaller seams based on intent.
|
||||
|
||||
| Seam inside a context | Purpose |
|
||||
|-----------------------|---------|
|
||||
| **Models** | Rich local state and pure operations |
|
||||
| **Policies** | Pure local decisions |
|
||||
| **Context workflows/tasks** | Local orchestration when a context needs it |
|
||||
| **Public API** | The intent surface other code is allowed to call |
|
||||
| **Interfaces / capabilities** | The internal contract between domain-facing code and effectful or replaceable collaborators |
|
||||
| **Adapters / registries / translators** | Boundary and implementation details |
|
||||
|
||||
At the repository top level, `src/workflows/` remains the place for **cross-context orchestration**.
|
||||
|
||||
### 3. Why this is better than one global onion tree
|
||||
A global folder tree keeps technical roles visible, but it weakens language ownership.
|
||||
You end up grouping code by purity category even when the more important question is, "Which domain language owns this concept?"
|
||||
|
||||
Context-first structure improves:
|
||||
- **Ownership clarity**: related concepts stay together
|
||||
- **Boundary reviewability**: cross-context interactions are easier to spot
|
||||
- **Refactoring safety**: a context can split or merge without rewriting the whole repo map
|
||||
|
||||
### 4. What still stays true
|
||||
The bounded-context shift did **not** repeal the old purity guidance.
|
||||
These points still stand:
|
||||
- pure decisions should stay out of I/O-heavy orchestration code
|
||||
- workflows should coordinate rather than bury business rules
|
||||
- interfaces/capabilities remain crucial internal seams
|
||||
- adapters should implement capabilities rather than own domain policy
|
||||
- registries are optional seams, not default architecture furniture
|
||||
|
||||
## Practical takeaway
|
||||
Think about architecture in two levels:
|
||||
1. **Outer boundary**: which bounded context owns the language?
|
||||
2. **Inner seams**: within that context, what should be a model, policy, workflow, translator, adapter, or public API?
|
||||
|
||||
That gives you the benefits of granular design without forcing the entire repo into one technical-layer-first shape.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Pure Logic vs Orchestration
|
||||
|
||||
## The Decision
|
||||
We still separate **decisions** from **mechanics**, but now we describe that separation in a context-first way:
|
||||
|
||||
- pure logic lives inside the bounded context that owns the language
|
||||
- top-level workflows coordinate across contexts
|
||||
- cross-context coordination should not erase local policy ownership
|
||||
|
||||
## The Reasoning
|
||||
|
||||
### 1. The "process vs rule" conflict still matters
|
||||
In many codebases, the rule "User must be over 18" is mixed with the code that "fetches the user from DB."
|
||||
That means to test the rule, you have to mock the DB.
|
||||
|
||||
We still separate them:
|
||||
- **Policy or model logic**: `canAccess(user)` -> pure function
|
||||
- **Workflow**: `fetchUser()` -> `canAccess(user)` -> `return`
|
||||
|
||||
### 2. Where pure logic belongs now
|
||||
The old docs sometimes implied that all decisions should be expressed through one top-level `src/policies/` folder.
|
||||
That is no longer the default.
|
||||
|
||||
Pure logic now usually lives in two places **inside a context**:
|
||||
1. **Models**: pure logic about one entity or state shape
|
||||
2. **Policies**: pure logic about decisions or relationships within that context
|
||||
|
||||
For cross-context workflows:
|
||||
- start with orchestration in the top-level workflow
|
||||
- keep cross-context rule logic inline until it earns a stable seam
|
||||
- only extract a top-level policy when the rule is genuinely about the relationship between contexts or the whole process
|
||||
|
||||
### 3. Determinism is still the payoff
|
||||
By keeping policy logic pure, we ensure that for any given input, the business decision is always the same.
|
||||
That makes testing easier, debugging easier, and review easier.
|
||||
|
||||
### 4. The workflow's job
|
||||
A workflow gathers data, invokes pure logic, coordinates effects, and makes the cross-context process visible.
|
||||
It should not quietly become the place where all business meaning accumulates.
|
||||
|
||||
That is why the repo now prefers:
|
||||
- local pure decisions inside contexts
|
||||
- public APIs at context boundaries
|
||||
- top-level workflows for visible cross-context coordination
|
||||
@@ -0,0 +1,90 @@
|
||||
# Architecture Rationale Summary
|
||||
|
||||
This page is the short version of why this template uses its current architecture.
|
||||
Use it as a refresher before design reviews, architecture discussions, or interviews.
|
||||
|
||||
## The central goal
|
||||
|
||||
The architecture is designed to make business logic easier to design, easier to review, and easier to change without dragging technical and collaborator concerns through every part of the codebase.
|
||||
|
||||
## The current shape
|
||||
|
||||
The repository is now optimized around **bounded contexts first**.
|
||||
That means the main organizing boundary is domain language ownership, not one global tree of layers.
|
||||
Purity still matters, but it mostly shows up **inside a context** and at **top-level workflow seams**.
|
||||
|
||||
## Why this structure exists
|
||||
|
||||
### Bounded contexts give language boundaries
|
||||
|
||||
A bounded context gives a local vocabulary, ownership seam, and review surface.
|
||||
That reduces hidden coupling and makes it easier to see where translation is required.
|
||||
|
||||
### Workflows should coordinate across contexts, not contexts calling each other directly
|
||||
|
||||
Cross-context business processes belong in top-level workflows.
|
||||
That keeps coordination visible and prevents one context from reaching into another context's internal language.
|
||||
|
||||
### Pure logic should stay easy to reason about
|
||||
|
||||
Inside a context, domain models and decisions should stay free of I/O where practical.
|
||||
That keeps the important rules easier to test, easier to review, and easier to trust.
|
||||
|
||||
### Types should carry business meaning
|
||||
|
||||
The design aims to make illegal states unrepresentable where practical.
|
||||
That is why the architecture prefers rich local domain types and explicit state variants instead of generic shapes plus status flags.
|
||||
|
||||
### Shared code should stay tiny
|
||||
|
||||
A type that looks reusable can become hidden cross-context coupling.
|
||||
The architecture therefore keeps `shared/` small and prefers translation at context boundaries.
|
||||
|
||||
## Why not one global layer tree
|
||||
|
||||
A global `src/domain/`, `src/policies/`, `src/adapters/`, and similar layout can keep purity visible, but it tends to weaken bounded language ownership.
|
||||
Over time it encourages unrelated concepts to sit beside each other just because they share a technical role.
|
||||
|
||||
This template now prefers:
|
||||
|
||||
- contexts as the first-class organizational boundary
|
||||
- local models, policies, and capability interfaces inside each context
|
||||
- top-level workflows for cross-context coordination
|
||||
- public APIs and translators at boundaries
|
||||
|
||||
## Why not events for everything
|
||||
|
||||
The architecture prefers direct composition for core application flow.
|
||||
Internal events are useful, but if they become the default glue, the system can lose local traceability and make refactoring harder.
|
||||
|
||||
The template therefore favors explicit function calls for the core path and reserves event-style thinking for domain outcomes and selected integration boundaries.
|
||||
|
||||
## Main tradeoffs
|
||||
|
||||
This architecture gives you:
|
||||
|
||||
- stronger language boundaries
|
||||
- lower hidden coupling
|
||||
- clearer review surfaces for cross-context flows
|
||||
- more explicit local domain models
|
||||
- safer localized refactoring
|
||||
|
||||
It also costs you:
|
||||
|
||||
- more up-front naming and boundary decisions
|
||||
- more translation at context edges
|
||||
- a need for discipline so `shared/` stays small and public APIs stay real
|
||||
|
||||
## What to say in an interview
|
||||
|
||||
A short summary is:
|
||||
|
||||
> We organize the system around bounded contexts so domain language and ownership stay explicit. Cross-context business processes are coordinated in top-level workflows, while pure decisions and state logic stay local to each context. We keep shared code small, translate at boundaries, and use rich types so business meaning stays reviewable and change remains localized.
|
||||
|
||||
## Where to go next
|
||||
|
||||
- Read `bounded-contexts.md` for the governing architecture statement.
|
||||
- Read `index.md` for the full architecture map.
|
||||
- Read `history.md` for alternatives considered and reframed.
|
||||
- Read `type-driven-design.md` for the type-modeling rationale.
|
||||
- Read `pure-logic.md` for the decision versus workflow split.
|
||||
@@ -0,0 +1,457 @@
|
||||
# Security Verification Rationale
|
||||
|
||||
This document explains why the repository now treats security review as an explicit verification activity and why the new security-review skills are shaped the way they are.
|
||||
|
||||
It is meant to be a durable reference for future contributors who ask questions such as:
|
||||
|
||||
- Why is security review a verification gate instead of a numbered TDFDDD phase?
|
||||
- Why are there two skills instead of one giant security reviewer?
|
||||
- Why is the review report structured the way it is?
|
||||
- Why do we emphasize trust boundaries, sinks, capability scope, and blast radius so heavily?
|
||||
- Why do we reject a single generic sanitizer in favor of context-specific handling?
|
||||
|
||||
## 1. Why security review was added
|
||||
|
||||
The original TDFDDD process already pushed the project toward strong design boundaries:
|
||||
|
||||
- parse untrusted data at the edge
|
||||
- keep domain logic pure
|
||||
- make illegal states harder to express
|
||||
- isolate infrastructure behind explicit seams
|
||||
|
||||
Those choices improve security indirectly because they make review easier and reduce accidental complexity.
|
||||
However, they do not by themselves guarantee secure software.
|
||||
|
||||
A codebase can still have:
|
||||
|
||||
- over-broad runtime authority
|
||||
- unsafe sink usage
|
||||
- missing recovery and detection planning
|
||||
- dangerous supply-chain assumptions
|
||||
- poor secret handling
|
||||
- security-sensitive omissions in tests and design docs
|
||||
|
||||
That is why the repository now treats security review as a first-class part of the development workflow.
|
||||
The goal is not to replace the design process with a security process.
|
||||
The goal is to add explicit checkpoints where reviewers can ask security-specific questions before risk silently ships.
|
||||
|
||||
## 2. Why security verification is a gate, not a core phase
|
||||
|
||||
The main TDFDDD phases exist to discover and freeze domain meaning:
|
||||
|
||||
1. discovery
|
||||
2. policy sketch
|
||||
3. domain modeling
|
||||
4. contract finalization
|
||||
5. implementation
|
||||
|
||||
Those phases answer questions like:
|
||||
|
||||
- What is the business action?
|
||||
- What information is required to decide it?
|
||||
- What states and events exist?
|
||||
- What is the contract between pure logic and orchestration?
|
||||
|
||||
Security review plays a different role.
|
||||
It does not create domain meaning.
|
||||
It evaluates whether the emerging design or implementation introduces avoidable risk.
|
||||
|
||||
That distinction matters.
|
||||
If security review were framed as just another design phase, it would encourage the wrong mental model:
|
||||
security would look like one more artifact to generate rather than a quality gate that can block, refine, or redirect work.
|
||||
|
||||
Calling it a **verification gate** keeps the relationship clear:
|
||||
|
||||
- the design phases build the artifact
|
||||
- the verification gate reviews the artifact
|
||||
- findings can send the work back for clarification or redesign
|
||||
|
||||
This is similar to testing, architecture review, or type-checking.
|
||||
They are essential to delivery, but they are not themselves the business-design phases.
|
||||
|
||||
## 3. Why there is a single skill with two phases
|
||||
|
||||
We chose to consolidate security review into one skill:
|
||||
|
||||
- `tdfddd-security-verification` (covers both Design and Implementation)
|
||||
|
||||
This was intentional to reduce instruction duplication, but internally it still branches its logic based on the phase.
|
||||
|
||||
### 3.1 Design review and implementation review answer different questions
|
||||
|
||||
Design review asks:
|
||||
|
||||
- Are trust boundaries visible?
|
||||
- Are capability boundaries explicit?
|
||||
- Are likely dangerous sinks named?
|
||||
- Are secrets, blast radius, recovery, and testing expectations discussed?
|
||||
- Is the proposed architecture setting up an implementation that can be secure?
|
||||
|
||||
Implementation review asks:
|
||||
|
||||
- Did the code actually parse untrusted input before treating it as trusted?
|
||||
- Did the implementation use safe sink patterns?
|
||||
- Did the workflow receive too much authority at runtime?
|
||||
- Did the tests cover the risky seam?
|
||||
- Did config or setup choices introduce additional exposure?
|
||||
|
||||
A single set of generic rules would blur these concerns.
|
||||
That would create two failure modes:
|
||||
|
||||
1. **False precision during design review**: the reviewer would start demanding code-level proof from artifacts that do not yet contain code.
|
||||
2. **Shallow code review during implementation**: the reviewer would spend too much time repeating design-level concerns instead of checking the concrete implementation.
|
||||
|
||||
The skill uses phase-specific heuristics so it can operate at the right level of evidence.
|
||||
|
||||
### 3.2 Early review is cheaper than late review
|
||||
|
||||
A security issue found in the design is usually cheaper to fix than the same issue found after implementation.
|
||||
|
||||
Examples:
|
||||
|
||||
- realizing that a workflow needs a read-only capability instead of a general admin capability
|
||||
- noticing that dangerous sinks exist but the ownership of sanitization/escaping is unspecified
|
||||
- discovering that secrets and operator assumptions are absent from the design
|
||||
- identifying that no detection or recovery story exists for the workflow
|
||||
|
||||
These are architecture-shaping issues.
|
||||
Catching them before assembly avoids rework and reduces the chance that an insecure pattern gets copied into multiple concrete implementations.
|
||||
|
||||
### 3.3 Review-only behavior is safer
|
||||
|
||||
Both skills are deliberately review-only.
|
||||
They do not auto-apply fixes.
|
||||
|
||||
This decision reflects a trust and workflow concern:
|
||||
security review often requires human judgment about context, tradeoffs, operational constraints, and false positives.
|
||||
A reviewer may correctly identify a risk but still misunderstand the broader product context.
|
||||
|
||||
By keeping the skills review-only:
|
||||
|
||||
- the human stays in control of interpretation
|
||||
- the review can raise questions before code changes happen
|
||||
- the system avoids auto-patching based on assumptions
|
||||
- remediation can be handled by a separate implementation pass once the human confirms direction
|
||||
|
||||
This matches the repository philosophy that design and review should reduce improvisation rather than automate it blindly.
|
||||
|
||||
## 4. Why the report is structured and evidence-based
|
||||
|
||||
The skills use a fixed report shape with sections such as:
|
||||
|
||||
- Scope Reviewed
|
||||
- Artifacts Read
|
||||
- Security Assumptions
|
||||
- Overall Risk Summary
|
||||
- one section per security concern category
|
||||
- Cross-Cutting Questions
|
||||
- Suggested Next Actions
|
||||
|
||||
The report also distinguishes between:
|
||||
|
||||
- **Finding**
|
||||
- **Possible Concern**
|
||||
- **Insufficient Evidence**
|
||||
|
||||
### 4.1 Why use a fixed format
|
||||
|
||||
A fixed structure prevents the review from skipping important categories just because the current feature is small or because one concern happens to dominate attention.
|
||||
|
||||
Without a fixed structure, reviews often become uneven:
|
||||
|
||||
- one reviewer focuses heavily on input validation but misses privilege boundaries
|
||||
- another focuses on secrets but ignores unsafe sinks
|
||||
- another talks only about abstract best practices without grounding them in the artifacts
|
||||
|
||||
The fixed format forces coverage consistency.
|
||||
It also makes reports easier to compare across features and easier for a human to skim.
|
||||
|
||||
### 4.2 Why include evidence levels
|
||||
|
||||
Security review always includes uncertainty.
|
||||
A reviewer may see a likely issue without enough evidence to state it as fact.
|
||||
If the skill is forced to choose only between “secure” and “insecure,” it will either hallucinate confidence or stay overly silent.
|
||||
|
||||
The three evidence levels solve that:
|
||||
|
||||
- **Finding** keeps the bar high for concrete claims.
|
||||
- **Possible Concern** makes room for reviewer intuition without overstating certainty.
|
||||
- **Insufficient Evidence** explicitly allows “I cannot responsibly assess this from what I read.”
|
||||
|
||||
This design reflects a key principle: honest uncertainty is better than invented precision.
|
||||
|
||||
### 4.3 Why findings include severity per issue
|
||||
|
||||
Each issue receives its own severity score rather than producing a single binary verdict.
|
||||
This supports prioritization.
|
||||
|
||||
A feature may have:
|
||||
|
||||
- one high-severity sink flaw
|
||||
- several medium-severity capability concerns
|
||||
- a handful of low-severity documentation omissions
|
||||
|
||||
Collapsing everything into pass/fail would hide useful nuance.
|
||||
Per-issue severity makes the output actionable without pretending that all flaws are equal.
|
||||
|
||||
## 5. Why the same master categories are used for both reviews
|
||||
|
||||
The design and implementation skills share the same master concern categories:
|
||||
|
||||
1. Trust Boundaries & Data Flow
|
||||
2. Input Parsing & Validation
|
||||
3. Sink Handling & Contextual Sanitization
|
||||
4. Capabilities & Least Privilege
|
||||
5. Secrets & Sensitive Config
|
||||
6. Isolation & Blast Radius
|
||||
7. Dependency & Supply Chain Assumptions
|
||||
8. AuthN/AuthZ if applicable
|
||||
9. Logging, Telemetry & Detection
|
||||
10. Recovery & Failure Modes
|
||||
11. Unsafe Dynamic Behavior
|
||||
12. Security Testing Coverage
|
||||
|
||||
The reason is consistency.
|
||||
|
||||
Contributors should not need one mental model for design review and a different one for implementation review.
|
||||
Using the same categories gives the process continuity:
|
||||
|
||||
- at design time you ask whether the category is visible and sufficiently specified
|
||||
- at implementation time you ask whether the code and config actually satisfy the category
|
||||
|
||||
This makes handoff cleaner.
|
||||
A design review finding in “Capabilities & Least Privilege” can later be checked concretely during implementation under the same category name.
|
||||
|
||||
## 6. Why context-specific sink handling was chosen over a generic sanitizer
|
||||
|
||||
One of the most important design decisions was to reject the idea of a single generic sanitizer.
|
||||
At first glance, a universal sanitizer sounds safer because it centralizes responsibility.
|
||||
In practice, it creates a false sense of completeness.
|
||||
|
||||
The problem is that different sinks interpret data differently.
|
||||
A value that is safe in one context may be dangerous in another.
|
||||
|
||||
Examples:
|
||||
|
||||
- HTML body rendering
|
||||
- HTML attribute rendering
|
||||
- JavaScript string interpolation
|
||||
- URL query construction
|
||||
- SQL statements
|
||||
- shell commands
|
||||
- file paths
|
||||
- email templates
|
||||
- logs
|
||||
|
||||
There is no single transformation that makes arbitrary input safe for every one of those interpreters.
|
||||
That is why the repository direction is:
|
||||
|
||||
- centralize the **policy and approved APIs by sink category**
|
||||
- use vetted libraries and safe framework primitives
|
||||
- keep the handling context-specific
|
||||
- prefer parameterized APIs where available instead of string escaping
|
||||
|
||||
This produces a better form of centralization:
|
||||
not “one sanitizer for everything,” but “one documented, reusable, approved strategy per sink class.”
|
||||
|
||||
### 6.1 Why generic sanitization is risky
|
||||
|
||||
A universal sanitizer encourages developers to think:
|
||||
|
||||
> this string is now clean forever
|
||||
|
||||
But security is not a permanent property of the raw string.
|
||||
It depends on how that data is used.
|
||||
|
||||
For example:
|
||||
|
||||
- SQL injection is best prevented with parameterized queries, not a magical sanitized string
|
||||
- XSS prevention depends on the output context and framework sink
|
||||
- shell safety depends on command construction strategy, not generic string cleanup
|
||||
- path safety depends on path semantics and allowlists, not generic character stripping
|
||||
|
||||
The right abstraction is usually not `SanitizedString`.
|
||||
It is more often one of:
|
||||
|
||||
- a parsed domain type
|
||||
- a context-specific safe output operation
|
||||
- a parameterized call into a safer API
|
||||
- a narrowly defined value object such as `EmailAddress`, `Url`, `Slug`, or `SqlIdentifier`
|
||||
|
||||
### 6.2 What gets centralized instead
|
||||
|
||||
Although a generic sanitizer was rejected, the project still benefits from centralization in these forms:
|
||||
|
||||
- a documented catalog of sink categories and approved defenses
|
||||
- reusable utilities for context-specific escaping where that is the correct defense
|
||||
- preferred libraries such as HTML sanitizers instead of handwritten logic
|
||||
- review rules that force the agent to inspect sink handling explicitly
|
||||
|
||||
This balances consistency with correctness.
|
||||
|
||||
## 7. Why capability scope and injected authority matter even in functional code
|
||||
|
||||
A major theme of the new docs is that purity is not the same thing as least privilege.
|
||||
This is an easy place for confusion.
|
||||
|
||||
In a functional architecture, code often looks clean because:
|
||||
|
||||
- dependencies are injected explicitly
|
||||
- data is immutable
|
||||
- workflows compose functions rather than mutating shared objects
|
||||
|
||||
Those are valuable properties.
|
||||
But they do not automatically limit runtime authority.
|
||||
|
||||
A workflow can still receive a capability that is broader than it needs.
|
||||
For example, a workflow that only needs to read data might be handed a service that can:
|
||||
|
||||
- read data
|
||||
- write data
|
||||
- delete data
|
||||
- access unrelated network systems
|
||||
- administer resources
|
||||
|
||||
The code is still functional.
|
||||
It is also still over-privileged.
|
||||
|
||||
That is why the review language emphasizes **capabilities** and **injected authority**.
|
||||
What matters is not only how pure the call graph is, but also what real-world powers the injected service represents.
|
||||
|
||||
### 7.1 What “injected authority” means here
|
||||
|
||||
Injected authority is the runtime power available through a dependency.
|
||||
This may include:
|
||||
|
||||
- database credentials with read/write/admin access
|
||||
- tokens that can call external APIs
|
||||
- filesystem access
|
||||
- shell execution access
|
||||
- network reachability to sensitive systems
|
||||
|
||||
The concern is not object-oriented statefulness.
|
||||
It is authority.
|
||||
A pure function can still orchestrate a dangerously powerful adapter.
|
||||
|
||||
### 7.2 Why small capabilities reduce blast radius
|
||||
|
||||
Narrow capabilities make compromise more containable.
|
||||
If a workflow only receives what it genuinely needs, then:
|
||||
|
||||
- accidental misuse is harder
|
||||
- malicious use has fewer options
|
||||
- review becomes more precise
|
||||
- tests can assert the intended seam more clearly
|
||||
|
||||
This is where the architecture helps.
|
||||
The repository’s use of explicit interfaces and workflow seams makes it easier to design small, purpose-specific capabilities rather than god-objects or god-services.
|
||||
The architecture does not guarantee least privilege automatically, but it makes it much easier to enforce.
|
||||
|
||||
## 8. Why blast radius is treated as a design concern, not just an ops concern
|
||||
|
||||
Blast radius is often discussed only in operational terms such as network segmentation, container boundaries, or IAM roles.
|
||||
Those matter, but application design also shapes blast radius.
|
||||
|
||||
Examples of design choices that affect blast radius:
|
||||
|
||||
- whether workflows depend on broad or narrow capabilities
|
||||
- whether dangerous sinks are concentrated and reviewable
|
||||
- whether secrets are globally available or tightly scoped
|
||||
- whether untrusted data becomes trusted only after explicit parsing
|
||||
- whether the system documents recovery and detection expectations
|
||||
|
||||
By reviewing blast radius at design time, the repository encourages contributors to ask:
|
||||
|
||||
- If this adapter or workflow is compromised, what else can it reach?
|
||||
- If this parsing boundary fails, where can unsafe data flow?
|
||||
- If this secret leaks, what authority does it grant?
|
||||
- If this workflow is abused, what bounded surface limits impact?
|
||||
|
||||
These are not only deployment questions.
|
||||
They are architectural questions.
|
||||
|
||||
## 9. Why manual activation was chosen
|
||||
|
||||
The new skills are manual rather than automatically triggered.
|
||||
This was chosen for two reasons.
|
||||
|
||||
### 9.1 Security review should be intentional
|
||||
|
||||
Security review can be expensive in both time and attention.
|
||||
Auto-triggering it on every related phrase would create noise and reduce trust in the output.
|
||||
Manual invocation makes the act deliberate.
|
||||
|
||||
### 9.2 Scope often needs clarification
|
||||
|
||||
Security review quality depends heavily on scope.
|
||||
A reviewer should know whether it is inspecting:
|
||||
|
||||
- only a single workflow design
|
||||
- a set of changed files
|
||||
- the adjacent seams
|
||||
- the wider project surface
|
||||
|
||||
Forcing manual activation makes it more natural to start with a short “grill the human” step if the requested scope is unclear.
|
||||
|
||||
## 10. Why the implementation review defaults to changed files plus adjacent seams
|
||||
|
||||
Implementation review can become expensive and noisy if it always expands to the whole repository.
|
||||
Most feature work changes a narrow slice.
|
||||
That means the security review should usually focus on:
|
||||
|
||||
- the changed workflow
|
||||
- touched policies, models, interfaces, services, layers, and tests
|
||||
- the nearby seam where risk actually appears
|
||||
|
||||
This keeps the review useful and proportionate.
|
||||
|
||||
Expanded-surface review still exists for times when a change touches broader concerns such as:
|
||||
|
||||
- dependency updates
|
||||
- setup scripts
|
||||
- CI configuration
|
||||
- agent/MCP config
|
||||
- deployment-related settings
|
||||
|
||||
But that broader review is opt-in because it should match the real change surface.
|
||||
|
||||
## 11. Why external standards are referenced
|
||||
|
||||
The skills explicitly allow and encourage references to external standards such as OWASP ASVS and OWASP cheat sheets.
|
||||
This was chosen to avoid two bad extremes:
|
||||
|
||||
1. a repo-local security process that reinvents common security guidance badly
|
||||
2. a review process that cites standards so generically that it stops being useful
|
||||
|
||||
External standards help anchor findings in established practice.
|
||||
Repo-local guidance then adapts those standards to this architecture.
|
||||
|
||||
This gives contributors both:
|
||||
|
||||
- a principled external reference
|
||||
- a concrete local interpretation
|
||||
|
||||
## 12. What this means for future work
|
||||
|
||||
The current decisions create a foundation, not a final destination.
|
||||
Future work may include:
|
||||
|
||||
- documenting approved sink-specific handling patterns in more detail
|
||||
- adding threat-model sections to design artifacts
|
||||
- creating a package-version lookup tool for agents so they can find recent versions before pinning
|
||||
- expanding the review checklist with more operational security prompts
|
||||
- documenting secure capability design examples for workflows and adapters
|
||||
|
||||
The important point is that security review is now explicit.
|
||||
It is part of how the template thinks, not just a separate external pipeline.
|
||||
|
||||
## 13. Short summary
|
||||
|
||||
The repository added security verification because architecture alone is not enough.
|
||||
It chose verification gates instead of a new numbered phase because security review evaluates artifacts rather than defining domain meaning.
|
||||
It split the work into design and implementation skills because they operate with different evidence and different goals.
|
||||
It made those skills review-only to preserve human judgment.
|
||||
It rejected a generic sanitizer in favor of sink-specific safe handling backed by centralized policy and approved APIs.
|
||||
And it emphasized narrow capabilities and injected authority because functional purity does not automatically provide least privilege.
|
||||
|
||||
These decisions aim to make the template more secure without making it vague, magical, or auto-patching by assumption.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Adapter Strategy: Public APIs, Adapters, and Registries
|
||||
|
||||
## The Decision
|
||||
In the bounded-context direction, the main seam is not a global domain-layer interface tree.
|
||||
The first seam is the **context's public API**.
|
||||
But inside a bounded context, **interfaces/capabilities are still a crucial seam** for isolating pure logic from effectful or replaceable collaborators and for keeping workflows reviewable.
|
||||
Inside or behind the public API, contexts may define capabilities, adapters, and optional registries as needed.
|
||||
|
||||
**Vocabulary Note:** In Effect, a "Layer" is a *constructor/provider* for an implementation, not the interface itself. `PostgresLive` is a Layer that constructs and provides the `Postgres` implementation.
|
||||
|
||||
## The Reasoning
|
||||
|
||||
### 1. The public API is the boundary seam
|
||||
Other contexts and top-level workflows should not know a context's internal files.
|
||||
They should call the context through a public entrypoint that expresses **intent** in that context's language.
|
||||
|
||||
That public API may internally depend on narrower capabilities, but those are not throwaway details.
|
||||
They are often the key seam that lets a context keep policy and workflow code stable while implementations and collaborators change behind it.
|
||||
|
||||
### 2. Interfaces are still the internal seam
|
||||
Domain logic should not know about `Stripe` or `Neo4j`.
|
||||
It should speak in the language of the owning context, while interfaces/capabilities define what collaborators can do without leaking implementation details.
|
||||
Adapters or other implementations then satisfy those capabilities.
|
||||
|
||||
So the preferred shape is:
|
||||
- **Public context API**: what other code may call
|
||||
- **Local capability/interface seam**: the contract the context depends on internally
|
||||
- **Adapter**: concrete implementation
|
||||
- **Layer**: wiring/provider
|
||||
|
||||
When a context talks to storage, external APIs, queues, filesystems, time, identity, configuration, feature flags, or other collaborators, an internal interface is often the thing that keeps:
|
||||
- workflows focused on orchestration instead of concrete implementation details
|
||||
- policy and model code free of effectful collaborator concerns
|
||||
- tests able to swap implementations cleanly
|
||||
- collaborator churn from rewriting domain logic
|
||||
|
||||
### 3. The naming problem still exists
|
||||
- **Bad**: another context imports `billing/adapters/StripeAdapter` directly
|
||||
- **Bad**: broad vague names like `SmallPaymentService`
|
||||
- **Good**: a top-level workflow calls the billing context's public API, and the billing context hides its wiring details
|
||||
|
||||
### 4. Strategy pattern and registries
|
||||
Sometimes implementation selection depends on domain data.
|
||||
That still does not belong in an adapter, and it should not be scattered through the workflow graph.
|
||||
|
||||
Use a registry only when dynamic runtime selection is a real seam:
|
||||
1. **Local policy** decides a strategy value
|
||||
2. **Registry** maps that strategy to an implementation of a capability/interface
|
||||
3. **Workflow or context API** asks the registry for the implementation
|
||||
|
||||
Registries are optional.
|
||||
Do not create them by default when direct wiring is enough.
|
||||
|
||||
## Practical takeaway
|
||||
Prefer this order of thought:
|
||||
1. What is the public intent API for this context?
|
||||
2. What capability/interface seams are needed behind it?
|
||||
3. Which adapters implement those capabilities?
|
||||
4. Is a registry actually needed, or would it be architecture furniture?
|
||||
|
||||
The important correction is: **bounded contexts replaced the global interface tree as the repository map, but they did not replace interfaces as a crucial internal seam.**
|
||||
@@ -0,0 +1,60 @@
|
||||
# Type-Driven Design & Rich Domain Models
|
||||
|
||||
## The Philosophy
|
||||
We follow the "Make Illegal States Unrepresentable" philosophy from Scott Wlaschin's *Domain Modeling Made Functional*.
|
||||
Instead of writing runtime checks for everything, we use the Type System to ensure that if code compiles, the data is likely valid.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### 1. Branded Types (Single-Case Union Models)
|
||||
Never use bare primitives for domain concepts. `string` is too broad for an Email. `number` is too dangerous for a Price.
|
||||
|
||||
- **Bad**: `const sendEmail = (to: string, body: string)` (Easy to mix up arguments)
|
||||
- **Good**: `const sendEmail = (to: Email, body: Body)`
|
||||
|
||||
In Effect, we use `Schema.brand` to create these distinct types with zero runtime overhead after validation.
|
||||
|
||||
### 2. Making Illegal States Unrepresentable
|
||||
If a User cannot have a "PaidDate" unless they have a "PaidStatus", do not make `paidDate` optional on the generic User. Create two types.
|
||||
|
||||
- **Bad**:
|
||||
```typescript
|
||||
type Order = {
|
||||
state: 'Unpaid' | 'Paid';
|
||||
paidAt?: Date; // Ambiguous: Can I have an Unpaid order with a paidAt date?
|
||||
}
|
||||
```
|
||||
|
||||
- **Good**:
|
||||
```typescript
|
||||
type UnpaidOrder = { state: 'Unpaid' }
|
||||
type PaidOrder = { state: 'Paid', paidAt: Date }
|
||||
type Order = UnpaidOrder | PaidOrder
|
||||
```
|
||||
Now it is *impossible* to access `paidAt` on an Unpaid order. The compiler forces you to check the state first.
|
||||
|
||||
### 3. Parse, Don't Validate
|
||||
"Validation" implies checking a value and returning true/false, but keeping the original untrusted type (e.g., `string`).
|
||||
"Parsing" implies checking a value and returning a *new, trusted type* (e.g., `Email`).
|
||||
|
||||
- **Workflow**:
|
||||
1. Receive `unknown` input.
|
||||
2. **Parse** it into a Domain Type (`Schema.decode`).
|
||||
3. Pass the Domain Type to your logic.
|
||||
4. Your logic never handles validation errors; it trusts the type.
|
||||
|
||||
### 4. The "Always Valid" Domain Model
|
||||
Functions in the `domain/` layer should generally assume their inputs are valid.
|
||||
- `calculateTotal(cart: Cart)`: Assumes `Cart` is a valid structure.
|
||||
- Validation happens at the *edges* (in the Workflow or Input Adapters), converting `RawJson` -> `Cart`.
|
||||
|
||||
### 5. Pipeline States (Intermediate Types)
|
||||
In Workflows, use specific types to represent the progress of a process. This ensures that steps cannot be executed out of order. You cannot "Ship" an order that hasn't been "Paid" because the `ship()` function demands a `PaidOrder` type, which is only created by the `pay()` function.
|
||||
|
||||
- **Pattern**:
|
||||
```typescript
|
||||
validateOrder(order: UnvalidatedOrder): ValidatedOrder // ensures address exists
|
||||
priceOrder(order: ValidatedOrder): PricedOrder // calculates taxes/totals
|
||||
makePayment(order: PricedOrder): PaidOrder // confirms transaction
|
||||
shipOrder(order: PaidOrder): ShippingConfirmation
|
||||
```
|
||||
@@ -0,0 +1,162 @@
|
||||
# Naming for Domain Modeling
|
||||
|
||||
Good naming is not cosmetic in TDFDDD.
|
||||
It is part of the design.
|
||||
If the names are vague, the model is usually vague too.
|
||||
|
||||
## Why names matter so much
|
||||
|
||||
The design artifacts are the thing the human reviewer audits.
|
||||
That means names must communicate domain meaning without requiring the reader to reverse-engineer intent from implementation details.
|
||||
|
||||
A weak name hides structure.
|
||||
A strong name exposes purpose, lifecycle, and boundaries.
|
||||
|
||||
## The three core naming laws
|
||||
|
||||
### 1. Lifecycle rule
|
||||
|
||||
Do not use one generic type name when the business actually cares about stages.
|
||||
|
||||
- Bad: `Item` with `status: "draft" | "open" | "sold"`
|
||||
- Better: `DraftItem | OpenItem | SoldItem`
|
||||
|
||||
If the state matters to the rules, the type names should usually reflect it.
|
||||
|
||||
### 2. Perspective rule
|
||||
|
||||
Name things by the role they play in the decision, not only by the data they happen to contain.
|
||||
|
||||
- Weak: `ItemWithBids`
|
||||
- Better: `BiddableItem`
|
||||
- Weak: `TotalBids`
|
||||
- Better: `BidHistory` or `BidCounter`
|
||||
|
||||
A good name answers the question: what is this for in the workflow?
|
||||
|
||||
### 3. Grammar rule
|
||||
|
||||
Use a stable grammar across artifacts:
|
||||
|
||||
- **Objects and states** are nouns: `Truck`, `LoadingTruck`, `BidHistory`
|
||||
- **Events** are past-tense facts: `PackageLoaded`, `BidPlaced`
|
||||
- **Commands** are imperative intents: `LoadPackage`, `PlaceBid`
|
||||
|
||||
This makes the design easier to scan and easier to review.
|
||||
|
||||
## Name the boundary, not just the data
|
||||
|
||||
A common failure mode is naming by storage shape rather than boundary meaning.
|
||||
|
||||
- Weak: `TruckData`
|
||||
- Better: `LoadingTruck`
|
||||
- Weak: `UserInfo`
|
||||
- Better: `AuthorizedUser` or `PendingUser`
|
||||
|
||||
The stronger name usually reveals which rules apply to the object.
|
||||
|
||||
## Prefer intent-first names over implementation-first names
|
||||
|
||||
A good name should describe what the caller is trying to accomplish, not the current internal shape of the model.
|
||||
|
||||
- Weak: `student.nextLesson()`
|
||||
- Better: `getNextTask(studentId)`
|
||||
- Weak: `markLessonComplete(studentId, lessonId)`
|
||||
- Better: `completeTask(studentId, taskId)`
|
||||
|
||||
The implementation may begin with lessons, but the workflow intent is often broader.
|
||||
Once the domain grows to include assessments, partial lessons, retries, or other progression units, an implementation-first name starts forcing one concept to masquerade as another.
|
||||
That is usually the beginning of model drift.
|
||||
|
||||
A practical review question is:
|
||||
|
||||
- Would this name still be correct if the underlying implementation changed but the user goal stayed the same?
|
||||
|
||||
If the answer is no, the name is probably too close to implementation detail.
|
||||
|
||||
## Intent-first naming applies beyond interfaces
|
||||
|
||||
This is not only about public APIs.
|
||||
The same rule helps with:
|
||||
|
||||
- **Functions**: `getNextTask` is usually stronger than `nextLesson`
|
||||
- **Types**: `ProgressItem` or `AssignableTask` may be stronger than `LessonRecord`
|
||||
- **Modules**: `advanceStudentProgress` is often stronger than `lessonSequencing`
|
||||
- **Workflows**: name the business outcome, not the current storage or transport representation
|
||||
|
||||
The goal is to make the code read in ubiquitous language even as the implementation evolves.
|
||||
|
||||
## Bounded seams should use the strongest names
|
||||
|
||||
Naming matters most at bounded seams.
|
||||
A bounded seam is the reviewable boundary where a human cares about the contract and is willing to treat the inside as a black box.
|
||||
|
||||
At those seams, names should communicate:
|
||||
|
||||
- the caller's intent
|
||||
- the capability or authority being exercised
|
||||
- the invariants that the boundary is protecting
|
||||
- the business meaning of the input and output
|
||||
|
||||
This is where vague names do the most damage.
|
||||
If the seam is named after implementation details, the whole area around it becomes harder to evolve safely.
|
||||
If the seam is named after intent and domain meaning, the implementation behind it can change with much less churn.
|
||||
|
||||
## Separate process from object
|
||||
|
||||
Do not mix workflow stages and domain nouns into one fuzzy container.
|
||||
|
||||
- Weak: `Video` with a `processed` flag
|
||||
- Better: `UnprocessedVideo | ProcessedVideo`
|
||||
- Weak: `Job` with a `state` string used everywhere
|
||||
- Better: `PendingJob | ActiveJob | CompletedJob`
|
||||
|
||||
If different stages have different allowed operations, model them as different states.
|
||||
|
||||
## Structural naming guidance
|
||||
|
||||
These conventions help keep the code and design artifacts aligned:
|
||||
|
||||
- **Workflows**: `verbNoun` or scenario names such as `checkout`, `registerUser`, `loadPackage`
|
||||
- **Policies**: decision verbs such as `decideDiscount`, `validateCart`, `authorizePayment`
|
||||
- **Pure model helpers**: entity-centered names such as `CartOps`, `MoneyOps`, `Truck`
|
||||
|
||||
## What the reviewer should look for
|
||||
|
||||
When reviewing LLM-generated design output, naming is often the quickest quality signal.
|
||||
|
||||
Be suspicious when you see:
|
||||
|
||||
- generic words like `data`, `info`, `manager`, `handler`, `processor`
|
||||
- objects carrying a broad `status` field that hides meaningful lifecycle states
|
||||
- events named like commands
|
||||
- commands named like processes
|
||||
- names that describe implementation detail instead of domain meaning
|
||||
- one domain concept stretched to cover unrelated future cases because the name is too narrow
|
||||
|
||||
A good reviewer asks whether the names would still make sense to a domain expert who never saw the code.
|
||||
|
||||
## The three-alternatives review rule
|
||||
|
||||
A useful way to pressure-test naming is to require three alternatives before settling on a seam, function, type, or workflow name.
|
||||
|
||||
For each alternative, ask:
|
||||
|
||||
- What caller intent does this name imply?
|
||||
- What domain model does this name assume?
|
||||
- What future changes would make this name misleading?
|
||||
- Is this naming a capability, an intent, or an implementation detail?
|
||||
|
||||
Example:
|
||||
|
||||
- `nextLesson` assumes the progression unit is a lesson
|
||||
- `nextAssignableUnit` is flexible but may be too abstract for the domain
|
||||
- `getNextTask` names the user-facing intent while leaving room for lessons, assessments, and partial work
|
||||
|
||||
The goal is not to maximize options.
|
||||
The goal is to make hidden assumptions visible early, while the seam is still cheap to change.
|
||||
|
||||
## Companion references
|
||||
|
||||
- Use `../reference/naming-lexicon.md` when you need vocabulary prompts.
|
||||
- Use `../reference/review-checklist.md` when reviewing complete design artifacts.
|
||||
@@ -0,0 +1,90 @@
|
||||
# TDFDDD Manifesto
|
||||
|
||||
Type-Driven Functional Domain-Driven Design is the design discipline behind this template.
|
||||
In casual conversation we call it **Functional Domain Modeling**.
|
||||
|
||||
## What the method is trying to achieve
|
||||
|
||||
The goal is not to move fast by skipping design.
|
||||
The goal is to make the important design decisions explicit enough that implementation becomes mechanical and review becomes reliable.
|
||||
|
||||
The core idea is simple:
|
||||
|
||||
> Make illegal states unrepresentable.
|
||||
|
||||
Instead of writing more and more runtime checks, use types and explicit state shapes so invalid situations are harder to express in the first place.
|
||||
|
||||
## The design pressure should come from the workflow
|
||||
|
||||
A common mistake is trying to invent the perfect domain model too early.
|
||||
That usually produces elegant-looking types that do not actually serve the workflow.
|
||||
|
||||
A better heuristic is:
|
||||
|
||||
> Let the workflow dictate the state.
|
||||
|
||||
Start with the decision the system must make.
|
||||
Sketch the policy signature with temporary or incomplete types if needed.
|
||||
Once you know what information the workflow needs, model those concepts precisely.
|
||||
|
||||
This keeps the design grounded in real use instead of speculation.
|
||||
|
||||
## Pure core, impure shell
|
||||
|
||||
We separate high-inference design work from mechanical implementation work.
|
||||
|
||||
- The **policy** is the pure core. It makes a domain decision from explicit inputs.
|
||||
- The **model** applies a state transition using pure data transformations.
|
||||
- The **workflow** is the impure shell. It gathers data, calls the policy, applies state changes, persists results, and invokes external capabilities.
|
||||
|
||||
This separation is what makes the method reviewable.
|
||||
A human can inspect the design in layers instead of trying to reason about everything at once.
|
||||
|
||||
## Design before code, but not design as guesswork
|
||||
|
||||
You should not jump straight into implementation code.
|
||||
But that does not mean you need a perfect domain ontology before you start.
|
||||
|
||||
A practical sequence is:
|
||||
|
||||
1. Write the story in domain language.
|
||||
2. Identify the command and possible events.
|
||||
3. Sketch the policy signature.
|
||||
4. Invent the minimum types required to make the decision precise.
|
||||
5. Freeze the contract.
|
||||
6. Translate the design into TypeScript and Effect.
|
||||
|
||||
That is why design artifacts matter.
|
||||
They preserve the reasoning that a human reviewer is validating.
|
||||
|
||||
## Why the human reviewer matters in an LLM-first workflow
|
||||
|
||||
In an LLM-first workflow, the model can produce the first draft of the design artifacts.
|
||||
The human still has to judge whether the artifacts are sound.
|
||||
|
||||
The reviewer is checking questions like these:
|
||||
|
||||
- Did the model identify the right command and outcomes?
|
||||
- Did it model state transitions instead of hiding them behind status flags?
|
||||
- Did it choose types that encode the business constraints?
|
||||
- Did it keep policies pure and move orchestration into workflows?
|
||||
- Did it return domain facts instead of vague booleans?
|
||||
|
||||
The review process only works if the repository preserves examples, templates, and clear standards for comparison.
|
||||
|
||||
## What a good finished design feels like
|
||||
|
||||
A good TDFDDD design usually has these properties:
|
||||
|
||||
- The story can be explained in domain language without mentioning framework details.
|
||||
- The command, events, and state variants are easy to name.
|
||||
- The policy signature makes the decision boundary obvious.
|
||||
- The final implementation looks like translation, not invention.
|
||||
- A reviewer can tell what is being verified at each phase.
|
||||
|
||||
## Where to go next
|
||||
|
||||
- If you want to learn by imitation, start with `../tutorials/worked-example-truck-loading.md`.
|
||||
- If you want to practice manually, use `../tutorials/practicing-tdfddd-by-hand.md`.
|
||||
- If you want the formal phase breakdown, read `./tdfddd-protocol.md`.
|
||||
- If you want to review model output, use `../how-to/review-an-llm-generated-design.md` and `../reference/review-checklist.md`.
|
||||
@@ -0,0 +1,267 @@
|
||||
# 2. The Design Protocol (TDFDDD)
|
||||
|
||||
**Type-Driven Functional Domain-Driven Design**
|
||||
|
||||
This is the rigorous process we use to discover, design, and implement features.
|
||||
Do not skip steps.
|
||||
Do not write implementation code until the design is frozen.
|
||||
|
||||
## How to read this page
|
||||
|
||||
This page is the **current protocol** for real work in this repository.
|
||||
It keeps the explanatory logic from the original 5-phase method, but rearranges that logic to fit the new **feature-first, slice-first** process.
|
||||
|
||||
- The old 5-phase design logic still exists as the conceptual backbone.
|
||||
- The repository workflow now applies that logic across **feature artifacts** and then **one workflow slice at a time**.
|
||||
- The simpler human practice flow is preserved in [../by-hand/README.md](../by-hand/README.md).
|
||||
|
||||
## The governing idea
|
||||
|
||||
The key shift is:
|
||||
|
||||
1. **Discover the feature broadly first** so bounded contexts, handoffs, and workflow inventory are visible.
|
||||
2. **Then design and implement one workflow slice at a time** inside one bounded context.
|
||||
|
||||
This keeps the rich domain reasoning from TDFDDD while avoiding monolithic feature-wide blueprints and bloated implementation passes.
|
||||
|
||||
## Phase 1: Feature Discovery
|
||||
|
||||
_Goal: Understand the feature as a whole before designing any single slice._
|
||||
|
||||
This phase inherits the old **event storming / discovery** logic, but applies it at the **feature level**.
|
||||
The job is to identify **what happens** in the business story without worrying about code yet.
|
||||
|
||||
### What to do
|
||||
|
||||
1. Identify the feature story, actors, commands, and outcomes.
|
||||
2. Identify the main business process and major domain facts.
|
||||
3. Capture feature-level business rules, invariants, and edge cases.
|
||||
4. Resolve the decision tree one branch at a time instead of jumping across many ambiguities at once.
|
||||
5. Ask clarifying questions one at a time, and periodically restate the current shared understanding before moving on.
|
||||
6. Identify candidate bounded contexts.
|
||||
7. Identify candidate workflow slices.
|
||||
8. When terminology stabilizes, record:
|
||||
- project-wide terms in `docs/reference/shared-language.md`
|
||||
- feature-level terms in `design/feature/<feature-slug>/discovery.md`
|
||||
9. Write `design/feature/<feature-slug>/discovery.md`.
|
||||
10. Create or update `design/feature/<feature-slug>/status.md`.
|
||||
|
||||
### Why this phase exists
|
||||
|
||||
If the feature story itself is muddy, everything downstream will be muddy too.
|
||||
A weak feature discovery causes false bounded contexts, confused workflow ownership, and shallow slice design later.
|
||||
|
||||
### Output
|
||||
|
||||
- `design/feature/<feature-slug>/discovery.md`
|
||||
- `design/feature/<feature-slug>/status.md`
|
||||
|
||||
## Phase 2: Context & Workflow Decomposition
|
||||
|
||||
_Goal: Map the feature into bounded contexts, workflow slices, and handoffs._
|
||||
|
||||
This is the major addition to the protocol.
|
||||
It sits between broad feature discovery and deep single-slice design.
|
||||
It exists so we do not accidentally core-sketch a workflow without understanding where it lives, what it owns, and what it hands off.
|
||||
|
||||
### What to do
|
||||
|
||||
1. Confirm the bounded contexts touched by the feature.
|
||||
2. Map feature steps to workflow slices.
|
||||
3. Record cross-context handoffs and orchestration notes in `design/feature/<feature-slug>/design.md`.
|
||||
4. Create or update `design/workflows/<bounded-context-slug>/shared-language.md` for each context involved.
|
||||
5. Create `design/workflows/<bounded-context-slug>/<workflow-slug>/01-decomposition.md` for each slice.
|
||||
6. Choose a recommended slice implementation order.
|
||||
7. Update the feature status file so the decomposition and slice inventory are visible.
|
||||
|
||||
### Why this phase exists
|
||||
|
||||
The old process worked well for a single workflow, but it was easy to over-apply it to a whole feature.
|
||||
That created large sketches, blurred bounded contexts, and implementation passes that were too big.
|
||||
Decomposition fixes that by making workflow ownership and handoffs explicit before deep design starts.
|
||||
|
||||
### Output
|
||||
|
||||
- `design/feature/<feature-slug>/design.md`
|
||||
- `design/workflows/<bounded-context-slug>/shared-language.md`
|
||||
- `design/workflows/<bounded-context-slug>/<workflow-slug>/01-decomposition.md`
|
||||
- updated `design/feature/<feature-slug>/status.md`
|
||||
|
||||
## Phase 3: Slice Discovery
|
||||
|
||||
_Goal: Interrogate one workflow slice deeply enough to sketch it well._
|
||||
|
||||
This phase brings the old discovery questions back, but only for one selected workflow slice.
|
||||
The point is to prevent the later F# sketch from being shallow or under-informed.
|
||||
|
||||
### What to do
|
||||
|
||||
1. Select exactly one workflow slice.
|
||||
2. Clarify the happy path, unhappy paths, invariants, and edge cases for that slice.
|
||||
3. Clarify what the slice owns versus what it only observes from other contexts.
|
||||
4. Record handoff assumptions.
|
||||
5. Restate the slice in plain domain language until the ownership boundary feels crisp.
|
||||
6. Update bounded-context shared language if terminology stabilizes.
|
||||
7. Write `design/workflows/<bounded-context-slug>/<workflow-slug>/02-discovery.md`.
|
||||
|
||||
### Why this phase exists
|
||||
|
||||
Your worry here is the right one: a core sketch is weak if the workflow-specific discovery is weak.
|
||||
Feature discovery should map the terrain.
|
||||
Slice discovery should interrogate the one path you are actually about to design.
|
||||
|
||||
### Output
|
||||
|
||||
- `design/workflows/<bounded-context-slug>/<workflow-slug>/02-discovery.md`
|
||||
- optional updates to `design/workflows/<bounded-context-slug>/shared-language.md`
|
||||
|
||||
## Phase 4: Core Sketch
|
||||
|
||||
_Goal: Figure out what information is required to make the decision for one workflow slice._
|
||||
|
||||
This is still the old **core sketch / policy signature** phase, now applied to exactly one bounded-context workflow.
|
||||
At this stage, the emphasis is still on **what information is needed**, not on fully modeled types.
|
||||
|
||||
### What to do
|
||||
|
||||
1. Draft the slice policy signature in **F# pseudo-code**.
|
||||
2. Determine the required owned state needed to answer the question.
|
||||
3. Identify any observed external inputs or handoff data that the slice may read.
|
||||
4. Record the command, events, and boundary notes.
|
||||
5. Write `design/workflows/<bounded-context-slug>/<workflow-slug>/03-core-sketch.md`.
|
||||
|
||||
### What this phase is not
|
||||
|
||||
- It is **not** the full domain model yet.
|
||||
- It is **not** implementation.
|
||||
- It is **not** cross-context orchestration design.
|
||||
|
||||
The job is to reveal the decision boundary clearly enough that modeling can become precise.
|
||||
|
||||
### Output
|
||||
|
||||
- `design/workflows/<bounded-context-slug>/<workflow-slug>/03-core-sketch.md`
|
||||
|
||||
## Phase 5: Blueprint
|
||||
|
||||
_Goal: Freeze the contract in F# for one workflow slice._
|
||||
|
||||
This phase combines the old **domain modeling** and **contract finalization** logic for one slice.
|
||||
F# remains the primary design language and frozen contract before assembly.
|
||||
|
||||
### What to do
|
||||
|
||||
1. Model the **primitives**.
|
||||
2. Model the **compounds**.
|
||||
3. Model the **aggregates / states**.
|
||||
4. Model the **events**.
|
||||
5. Finalize the signatures for:
|
||||
- `decide`
|
||||
- `apply`
|
||||
- the workflow boundary
|
||||
6. Treat the F# blueprint as the authoritative design contract.
|
||||
7. Write `design/workflows/<bounded-context-slug>/<workflow-slug>/04-blueprint.fs`.
|
||||
8. Update `design/workflows/<bounded-context-slug>/shared-model.fs` only when the context gains reusable shared F# pieces.
|
||||
|
||||
### Design standards in this phase
|
||||
|
||||
- Avoid primitive obsession.
|
||||
- Make illegal states unrepresentable.
|
||||
- Keep the policy pure.
|
||||
- Model outputs as domain facts, not booleans.
|
||||
- Separate:
|
||||
- policy for pure decisions
|
||||
- model for pure state transition math
|
||||
- workflow for impure orchestration
|
||||
|
||||
### Why F# matters here
|
||||
|
||||
F# is not an optional flavoring step.
|
||||
It is the design language that makes the contract precise before implementation starts.
|
||||
If the blueprint is weak, assembly will start inventing domain meaning instead of translating it.
|
||||
|
||||
### Output
|
||||
|
||||
- `design/workflows/<bounded-context-slug>/<workflow-slug>/04-blueprint.fs`
|
||||
- optional updates to `design/workflows/<bounded-context-slug>/shared-model.fs`
|
||||
|
||||
## Phase 6: Assembly
|
||||
|
||||
_Goal: Translate one frozen F# blueprint into code._
|
||||
|
||||
This is still the old implementation / assembly phase, but it now runs on **exactly one workflow slice at a time**.
|
||||
Assembly is a translation step from the frozen F# contract, not a redesign step.
|
||||
|
||||
### What to do
|
||||
|
||||
1. Read one `04-blueprint.fs` file.
|
||||
2. Translate F# types to Effect schemas and TypeScript constructs.
|
||||
3. Derive executable specifications from the frozen contract:
|
||||
- workflow scenarios from the command and events
|
||||
- policy examples from the decision rules
|
||||
- model invariants from the state definitions
|
||||
4. Implement using vertical **RED -> GREEN -> REFACTOR** cycles:
|
||||
- write one failing test for one observable behavior at the public seam
|
||||
- write the minimal code to make it pass
|
||||
- refactor only while green
|
||||
5. Keep tests focused on behavior through the contract, not implementation details.
|
||||
6. Concentrate review rigor at the design and seam level.
|
||||
7. Update `design/feature/<feature-slug>/status.md` for the current slice.
|
||||
|
||||
### Test style by seam
|
||||
|
||||
- **Models / Policies (Pure):** example-based tests plus property-based tests for invariants and edge cases.
|
||||
- **Workflows (Impure):** scenario tests using Effect test layers or in-memory adapters.
|
||||
- **Services / Adapters:** contract tests and integration tests against real infrastructure when needed.
|
||||
|
||||
### Review emphasis
|
||||
|
||||
- Review policies and workflows closely because they carry business meaning and orchestration correctness.
|
||||
- Review service and adapter internals mainly for seam correctness, capability behavior, and absence of misplaced business logic.
|
||||
- Trust strong domain types and contracts to reduce how much infrastructure code must be re-derived line by line.
|
||||
|
||||
## Rules that apply across phases
|
||||
|
||||
- One workflow slice belongs to one bounded context.
|
||||
- No cross-context decision logic belongs inside a workflow slice.
|
||||
- Cross-context behavior belongs in feature-level handoff and orchestration notes.
|
||||
- Shared language should exist both globally and per bounded context.
|
||||
- F# remains the primary design language and frozen contract before assembly.
|
||||
- Implementation should proceed one workflow slice at a time.
|
||||
- For the exact artifact layout, see [../reference/design-artifact-structure.md](../reference/design-artifact-structure.md).
|
||||
|
||||
## Security Verification
|
||||
|
||||
Security review is a required part of the process even though it is not itself a core design phase.
|
||||
It acts as a verification gate around the design and implementation work.
|
||||
|
||||
### Design security verification
|
||||
|
||||
Run a security review on the frozen design before or during the transition from blueprint to assembly, when the slice has enough shape to inspect.
|
||||
|
||||
Review at least:
|
||||
|
||||
- the workflow story and trust boundaries
|
||||
- the domain model and capability boundaries
|
||||
- sink categories and sanitization ownership
|
||||
- secrets handling assumptions
|
||||
- blast-radius and isolation assumptions
|
||||
- detection, recovery, and security test expectations
|
||||
|
||||
In Amp, use the `tdfddd-security-verification` skill when you want a structured, review-only report.
|
||||
|
||||
### Implementation security verification
|
||||
|
||||
Run a second security review after implementation on the changed workflow slice and its adjacent seams.
|
||||
Expand the scope only when needed.
|
||||
|
||||
Review at least:
|
||||
|
||||
- untrusted input parsing into trusted domain types
|
||||
- dangerous sinks and context-specific protections
|
||||
- over-broad capabilities or injected authority
|
||||
- secret and sensitive-config handling
|
||||
- unsafe dynamic behavior
|
||||
- security-focused test coverage at risky seams
|
||||
|
||||
In Amp, use the `tdfddd-security-verification` skill for a structured, review-only report over code and nearby surfaces.
|
||||
@@ -0,0 +1,132 @@
|
||||
# TDFDDD Refactoring Protocol
|
||||
|
||||
This is the process for evolving existing code without losing correctness or reviewability.
|
||||
It complements the design protocol.
|
||||
The design protocol helps you create a good shape.
|
||||
The refactoring protocol helps you recover, improve, or reshape that shape as the system grows.
|
||||
|
||||
Do not treat refactoring as random cleanup.
|
||||
Use a phased process with an explicit scope, preserved behavior, and reviewable seam decisions.
|
||||
|
||||
## The Checklist
|
||||
|
||||
### Phase 0: Scope Gate
|
||||
|
||||
_Goal: Choose the size of the refactor and define what must stay true._
|
||||
|
||||
1. Pick one scope:
|
||||
- function
|
||||
- task
|
||||
- workflow
|
||||
- module
|
||||
- cross-module slice
|
||||
2. State the preserved behavior.
|
||||
3. State the non-goals.
|
||||
4. State the success boundary:
|
||||
- what will be easier to review
|
||||
- what seam will become clearer
|
||||
- what authority or dependency surface should shrink
|
||||
|
||||
### Phase 1: Change Pressure
|
||||
|
||||
_Goal: Identify where the current shape is fighting real change._
|
||||
|
||||
Look for:
|
||||
|
||||
- code that changes together across multiple edits
|
||||
- code that should change independently but is tangled together
|
||||
- pass-through parameters that leak mechanics
|
||||
- repeated branching at one boundary
|
||||
- top-level workflows accumulating too much coordination detail
|
||||
|
||||
Without automation, keep this lightweight.
|
||||
Use recent edits, current pain, and active feature pressure rather than trying to analyze the whole repository.
|
||||
|
||||
### Phase 2: Seam Diagnosis
|
||||
|
||||
_Goal: Explain what is wrong with the current boundary before changing it._
|
||||
|
||||
For each candidate seam, ask:
|
||||
|
||||
- What is its real purpose?
|
||||
- What should the caller know, and what should stay hidden?
|
||||
- Is the name intention-based or mechanical?
|
||||
- Is this really a policy, model, service, task, or module?
|
||||
- Does it require too much authority?
|
||||
- Does it force the caller to understand internal details?
|
||||
- Would three likely variations still fit this boundary naturally?
|
||||
|
||||
A seam is weak when it only moves code around without reducing context load.
|
||||
|
||||
### Phase 3: Target Shape
|
||||
|
||||
_Goal: Define the new shape before large moves begin._
|
||||
|
||||
Describe:
|
||||
|
||||
- the new seam name
|
||||
- the new public contract
|
||||
- what stays internal
|
||||
- what dependencies are allowed
|
||||
- whether the result should be a policy, model operation, service, task, or module
|
||||
|
||||
Prefer intention-based APIs.
|
||||
Name the boundary by what the caller is trying to accomplish, not by how the work is performed.
|
||||
|
||||
### Phase 4: Safety Net
|
||||
|
||||
_Goal: Lock observable behavior before reshaping internals._
|
||||
|
||||
1. Add or tighten tests at the public seam.
|
||||
2. Preserve domain behavior first.
|
||||
3. Let types define what cannot be broken silently.
|
||||
4. If behavior is unclear, stop and clarify before moving code.
|
||||
|
||||
The purpose of the safety net is not perfect coverage.
|
||||
It is to make the next move safe enough to review confidently.
|
||||
|
||||
### Phase 5: Reshape
|
||||
|
||||
_Goal: Change the structure in small verified moves._
|
||||
|
||||
1. Move one responsibility at a time.
|
||||
2. Keep the code running after each move.
|
||||
3. Extract policies and model operations aggressively when pure logic appears.
|
||||
4. Extract tasks when orchestration steps gain stable intent.
|
||||
5. Promote a bounded module only when the seam repeatedly proves useful.
|
||||
6. Prefer public APIs for cross-module calls.
|
||||
|
||||
Do not introduce more layers than the current pressure justifies.
|
||||
|
||||
### Phase 6: Proof
|
||||
|
||||
_Goal: Show that the refactor improved the shape without breaking behavior._
|
||||
|
||||
Verify:
|
||||
|
||||
- preserved behavior still holds
|
||||
- tests and types still pass
|
||||
- the seam is easier to explain
|
||||
- the caller now needs less context
|
||||
- the dependency or authority surface is smaller or clearer
|
||||
|
||||
A successful refactor should reduce review load, not merely relocate code.
|
||||
|
||||
## One Protocol, Multiple Scales
|
||||
|
||||
Use the same protocol at different sizes.
|
||||
Only the scope changes.
|
||||
|
||||
- **Micro:** function, type, parameter list, local extraction
|
||||
- **Meso:** task, workflow, module seam
|
||||
- **Macro:** cross-module or bounded-context reshaping
|
||||
|
||||
The questions stay the same.
|
||||
The blast radius changes.
|
||||
|
||||
## Manual First, Tool Later
|
||||
|
||||
A future tool may help identify change pressure by mining coordinated edits.
|
||||
Do not block on that tool.
|
||||
Start manually with current pain and recent changes.
|
||||
Automation should strengthen judgment, not replace it.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Why Events, Not Booleans
|
||||
|
||||
A policy should usually return a rich domain fact, not just `true` or `false`.
|
||||
|
||||
## The problem with booleans
|
||||
|
||||
A boolean only tells you whether something was allowed.
|
||||
It does not tell you what happened, why it happened, or what data the rest of the system needs next.
|
||||
|
||||
For example, this policy is usually too thin:
|
||||
|
||||
```ts
|
||||
const canLoadPackage = (truck: LoadingTruck, pkg: Package): boolean => {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The workflow now has to reconstruct meaning from the inputs and repeat domain reasoning elsewhere.
|
||||
|
||||
## What an event gives you
|
||||
|
||||
A better policy returns a success event or a failure event.
|
||||
|
||||
```ts
|
||||
Result<PackageLoaded, LoadFailure>
|
||||
```
|
||||
|
||||
That gives the workflow concrete domain facts:
|
||||
|
||||
- `PackageLoaded` says what succeeded
|
||||
- `LoadFailure` says why the decision failed
|
||||
- the event payload can carry the exact information needed for the next step
|
||||
|
||||
This keeps the workflow mechanical.
|
||||
It does not need to invent new business meaning after the decision has already been made.
|
||||
|
||||
## Events improve reviewability
|
||||
|
||||
Events are also easier for humans to review than booleans.
|
||||
A reviewer can ask:
|
||||
|
||||
- Are these the right outcomes?
|
||||
- Does each outcome carry the data the system needs next?
|
||||
- Are we returning domain facts instead of UI phrasing or infrastructure detail?
|
||||
|
||||
Those are much easier questions to answer when the outputs are explicit event types.
|
||||
|
||||
## Events are data, not instructions
|
||||
|
||||
An event is a fact about what happened.
|
||||
It is not a command to perform side effects.
|
||||
|
||||
- Good: `PackageLoaded`
|
||||
- Bad: `SendPackageLoadedEmail`
|
||||
|
||||
The workflow decides what to do with the fact in context.
|
||||
That is where orchestration belongs.
|
||||
|
||||
## A good rule of thumb
|
||||
|
||||
If a policy output would force the workflow to ask "yes, but what exactly happened?", return an event instead of a boolean.
|
||||
Reference in New Issue
Block a user