Initial commit
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# Effect TDFDDD Template Documentation
|
||||
|
||||
Welcome to the documentation for the Effect Type-Driven Functional Domain-Driven Design template.
|
||||
This repo uses a modified [Diátaxis](https://diataxis.fr/) structure so the docs serve both of these jobs:
|
||||
|
||||
- teach humans how to understand and practice the method
|
||||
- help humans review LLM-generated design artifacts with confidence
|
||||
|
||||
## Start here by goal
|
||||
|
||||
### I want to learn the method by hand
|
||||
|
||||
Read these in order:
|
||||
|
||||
1. [TDFDDD Manifesto](explanation/tdfddd-manifesto.md)
|
||||
2. [By-hand practice guide](by-hand/README.md)
|
||||
3. [Worked Example: Truck Loading](tutorials/worked-example-truck-loading.md)
|
||||
4. [Practicing TDFDDD by Hand](tutorials/practicing-tdfddd-by-hand.md)
|
||||
5. [Design Artifact Template](reference/design-artifact-template.md)
|
||||
|
||||
### I want to review LLM-generated design output
|
||||
|
||||
Read these in order:
|
||||
|
||||
1. [TDFDDD Manifesto](explanation/tdfddd-manifesto.md)
|
||||
2. [Worked Example: Truck Loading](tutorials/worked-example-truck-loading.md)
|
||||
3. [How to Review an LLM-Generated Design](how-to/review-an-llm-generated-design.md)
|
||||
4. [Review Checklist for TDFDDD Artifacts](reference/review-checklist.md)
|
||||
5. [TDFDDD Protocol](explanation/tdfddd-protocol.md) for the design and implementation security verification gates
|
||||
|
||||
## Tutorials
|
||||
|
||||
_Learning-oriented guides for building intuition through examples and practice._
|
||||
|
||||
- [Effect Starting Guide](tutorials/effect-starting-guide.md)
|
||||
- [Worked Example: Truck Loading](tutorials/worked-example-truck-loading.md)
|
||||
- [Practicing TDFDDD by Hand](tutorials/practicing-tdfddd-by-hand.md)
|
||||
|
||||
## How-to guides
|
||||
|
||||
_Task-oriented guides for doing specific work._
|
||||
|
||||
- [Implementation Guide](how-to/implementation-guide.md)
|
||||
- [How to Review an LLM-Generated Design](how-to/review-an-llm-generated-design.md)
|
||||
|
||||
## Explanation
|
||||
|
||||
_Understanding-oriented material for the ideas, tradeoffs, and philosophy behind the method._
|
||||
|
||||
- [TDFDDD Manifesto](explanation/tdfddd-manifesto.md)
|
||||
- [TDFDDD Protocol](explanation/tdfddd-protocol.md)
|
||||
- [Naming for Domain Modeling](explanation/naming-for-domain-modeling.md)
|
||||
- [Why Events, Not Booleans](explanation/why-events-not-booleans.md)
|
||||
- [Architecture Reasoning](explanation/architecture/index.md)
|
||||
- [Bounded Context Architecture Statement](explanation/architecture/bounded-contexts.md)
|
||||
- [Architecture Rationale Summary](explanation/architecture/rationale-summary.md)
|
||||
- [Security Verification Rationale](explanation/architecture/security-verification-rationale.md)
|
||||
|
||||
## Reference
|
||||
|
||||
_Information-oriented material for lookups, templates, and stable rules._
|
||||
|
||||
- [Directory Layout](reference/directory-layout.md)
|
||||
- [Project Structure Overview](reference/overview.md)
|
||||
- [Conventions](reference/conventions.md)
|
||||
- [Rule Examples](reference/rule-examples.md)
|
||||
- [Naming Lexicon](reference/naming-lexicon.md)
|
||||
- [Review Checklist for TDFDDD Artifacts](reference/review-checklist.md)
|
||||
- [Design Artifact Template](reference/design-artifact-template.md)
|
||||
- [Design Artifact Structure](reference/design-artifact-structure.md)
|
||||
|
||||
## By-hand practice
|
||||
|
||||
_Human-first material for learning the original 5-phase design flow manually._
|
||||
|
||||
- [By-hand practice guide](by-hand/README.md)
|
||||
|
||||
---
|
||||
|
||||
## Agent Guidance
|
||||
|
||||
If you are an AI agent working in this repository, consult `.agents/` or `AGENTS.md` for operational rules. Contextual rules are loaded based on the files you edit.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Practicing TDFDDD by Hand
|
||||
|
||||
This directory preserves the human-first **5-phase** practice flow.
|
||||
Use it when you want to learn the design method manually, on paper or in scratch markdown, without the full feature-to-slice artifact workflow used by the agent pipeline.
|
||||
|
||||
## What stays here
|
||||
|
||||
These docs are intentionally optimized for **human practice**:
|
||||
|
||||
1. Event storming / discovery
|
||||
2. Core sketch
|
||||
3. Domain modeling
|
||||
4. Contract
|
||||
5. Implementation thinking
|
||||
|
||||
They are not the source of truth for the agent artifact layout.
|
||||
The operational artifact structure for real feature work now lives under:
|
||||
|
||||
- `design/feature/<feature-slug>/...`
|
||||
- `design/workflows/<bounded-context-slug>/<workflow-slug>/...`
|
||||
|
||||
## Read these in order
|
||||
|
||||
- `../tutorials/worked-example-truck-loading.md`
|
||||
- `../tutorials/practicing-tdfddd-by-hand.md`
|
||||
- `../reference/design-artifact-template.md`
|
||||
|
||||
## How to use both doc sets together
|
||||
|
||||
- Use `docs/by-hand/` to build judgment and practice the 5 design phases manually.
|
||||
- Use the main docs for the current feature/slice artifact structure, bounded-context decomposition, and review of generated artifacts.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,167 @@
|
||||
# 4. Implementation Guide
|
||||
|
||||
**Goal:** Mechanical translation from F# Design to TypeScript/Effect Code, using TDD during implementation.
|
||||
|
||||
## TDD Inside Phase 5
|
||||
|
||||
Keep the F# design fixed first. TDD starts only after the contract is frozen.
|
||||
|
||||
Use short vertical cycles:
|
||||
|
||||
1. Pick one behavior from the contract.
|
||||
2. Write one failing test at the module's public seam.
|
||||
3. Implement the smallest change that passes.
|
||||
4. Refactor only after the test suite is green.
|
||||
5. Avoid premature indirection: keep logic combined by default, and extract only when the green code shows a real gain in nameability, testability, reuse, boundary clarity, or duplicated complexity.
|
||||
6. Good names help reveal useful extractions, but they do not justify an unnecessary layer by themselves.
|
||||
|
||||
Choose the test style by seam:
|
||||
|
||||
- **context-local models and policies**: favor example-based tests plus property-based tests for invariants, boundary conditions, and illegal-state protection.
|
||||
- **top-level `src/workflows/`**: favor scenario tests using Effect layers or in-memory adapters. Verify the workflow's observable result, not internal calls.
|
||||
- **context adapters**: use contract tests for shared adapter behavior and integration tests when talking to real infrastructure.
|
||||
|
||||
The frozen design tells you what to test:
|
||||
|
||||
- **Command + Events** -> workflow scenarios
|
||||
- **Policy signature** -> decision examples and failure cases
|
||||
- **State model** -> invariants and state-transition properties
|
||||
|
||||
The point is not to replace TDFDDD with TDD. The design process decides _what must be true_; TDD drives the safe, incremental implementation of that design.
|
||||
|
||||
## Slice Implementation by Workflow
|
||||
|
||||
The current artifact structure already assumes **one F# blueprint per workflow slice**.
|
||||
Do not combine multiple workflow slices into one assembly pass.
|
||||
|
||||
Prefer one workflow slice per implementation pass:
|
||||
|
||||
- one `04-blueprint.fs` contract
|
||||
- one focused set of tests
|
||||
- one reviewable code change
|
||||
- one fresh context if the work is large enough to benefit from it
|
||||
|
||||
This keeps context windows smaller, review easier, and generated code more reliable.
|
||||
The feature-level status file plus the workflow-slice artifacts should be enough to let a new context pick up the next slice safely.
|
||||
|
||||
## Review Focus After Implementation
|
||||
|
||||
Review rigor should concentrate at the places where business meaning and system shape live:
|
||||
|
||||
- **Review closely:** contracts, domain types, policies, workflows, and adapter seams.
|
||||
- **Review more lightly:** service and adapter internals, as long as they satisfy the seam, keep business logic out, and behave correctly through tests.
|
||||
|
||||
This is not permission to be sloppy in infrastructure code. The goal is locality: when types are strong, policies are pure, and seams are well-designed, a reviewer should not need to re-derive the whole business model from inside each adapter.
|
||||
|
||||
## The Pattern: Decide -> Match -> Apply
|
||||
|
||||
This is the standard orchestration pattern for all workflows. It keeps the rules pure, the math pure, and the side effects isolated.
|
||||
|
||||
### 1. The Policy (Pure Rules)
|
||||
|
||||
_Located in: the owning bounded context by default; only top-level when the seam is truly cross-context_
|
||||
|
||||
A pure decision should usually still be a function, but it does **not** always need its own top-level policy module.
|
||||
Keep a decision as a local pure function inside the owning context when that is the clearest choice.
|
||||
Promote it to a broader seam only when extraction materially improves **nameability, testability, reuse, or boundary clarity**.
|
||||
Prefer local logic when extraction would only add indirection.
|
||||
|
||||
```typescript
|
||||
// LoadPolicy.ts
|
||||
export const decide = (truck: LoadingTruck, pkg: Package): Result<PackageLoaded, LoadFailure> => {
|
||||
// 1. Check Rules
|
||||
if (truck.currentLoad.weight + pkg.weight > truck.capacity.maxWeight) {
|
||||
return Result.fail({ tag: "LoadFailure", reason: "OverWeight", ... })
|
||||
}
|
||||
|
||||
if (truck.currentLoad.volume + pkg.volume > truck.capacity.maxVolume) {
|
||||
return Result.fail({ tag: "LoadFailure", reason: "InsufficientVolume", ... })
|
||||
}
|
||||
|
||||
// 2. Return Success Event (Do NOT calculate new state here, just return the fact)
|
||||
return Result.succeed({
|
||||
tag: "PackageLoaded",
|
||||
package: pkg,
|
||||
truckId: truck.id
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2. The Model (Pure Math)
|
||||
|
||||
_Located in: the owning bounded context's local model seam_
|
||||
|
||||
```typescript
|
||||
// Truck.ts
|
||||
// The "Reducer" - takes state + data -> new state
|
||||
export const applyLoad = (truck: LoadingTruck, pkg: Package): LoadingTruck => ({
|
||||
...truck,
|
||||
currentLoad: {
|
||||
weight: truck.currentLoad.weight + pkg.weight,
|
||||
volume: truck.currentLoad.volume + pkg.volume,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 3. The Workflow (Impure Shell)
|
||||
|
||||
_Located in: the owning context for local orchestration, or top-level `src/workflows/` for cross-context orchestration_
|
||||
|
||||
```typescript
|
||||
// LoadWorkflow.ts
|
||||
const loadPackageWorkflow = (truckId: TruckId, packageId: PackageId) =>
|
||||
Effect.gen(function* (_) {
|
||||
// 0. Get Dependencies
|
||||
const repo = yield* Database;
|
||||
|
||||
// 1. Gather Data (IO)
|
||||
const truck = yield* _(repo.getTruck(truckId));
|
||||
const pkg = yield* _(repo.getPackage(packageId));
|
||||
|
||||
// 2. Execute Policy (Decide)
|
||||
// The Workflow does not know logic. It just asks the Policy.
|
||||
const decision = LoadPolicy.decide(truck, pkg);
|
||||
|
||||
// 3. Match & Apply (Orchestration)
|
||||
return yield* _(
|
||||
Match.value(decision).pipe(
|
||||
// CASE: SUCCESS
|
||||
Match.when({ _tag: "PackageLoaded" }, (event) =>
|
||||
Effect.gen(function* (_) {
|
||||
// A. Apply the change (Pure Math)
|
||||
// The Policy said "Yes", so we calculate the new state.
|
||||
const newTruckState = Truck.applyLoad(truck, event.package);
|
||||
|
||||
// B. Persist the new state (IO)
|
||||
yield* _(repo.saveTruck(newTruckState));
|
||||
|
||||
// C. Return the response
|
||||
return {
|
||||
success: true,
|
||||
updatedCapacity: newTruckState.currentLoad,
|
||||
};
|
||||
}),
|
||||
),
|
||||
|
||||
// CASE: FAILURE
|
||||
Match.when({ _tag: "LoadFailure" }, (failure) =>
|
||||
// We can choose to return a failure response OR fail the effect
|
||||
Effect.fail(new BusinessError(failure.reason)),
|
||||
),
|
||||
|
||||
Match.exhaustive,
|
||||
),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
## Translation Table
|
||||
|
||||
| Concept | F# Design | TypeScript / Effect Implementation |
|
||||
| :------------ | :----------------------------- | :---------------------------------------------------- |
|
||||
| **Primitive** | `type Weight = int<kg>` | `type Weight = number & Brand<"Kg">` |
|
||||
| **Structure** | `type User = { Name: string }` | `const User = Schema.Struct({ name: Schema.String })` |
|
||||
| **Union** | `type State = A \| B` | `Schema.Union(A, B)` (Discriminated Union) |
|
||||
| **Function** | `Input -> Output` | `(input: Input) => Output` |
|
||||
| **Result** | `Result<Success, Error>` | `Either<Error, Success>` (Effect's Either) |
|
||||
| **Async** | `Async<Result<T, E>>` | `Effect<T, E>` |
|
||||
@@ -0,0 +1,87 @@
|
||||
# How to Refactor with TDFDDD
|
||||
|
||||
Use this guide when existing code no longer matches the workflow graph cleanly.
|
||||
The goal is to preserve behavior while improving seams, names, and reviewability.
|
||||
|
||||
## Before you start
|
||||
|
||||
Write down five things:
|
||||
|
||||
1. **Scope:** function, task, workflow, module, or cross-module slice
|
||||
2. **Preserved behavior:** what must remain true
|
||||
3. **Pain:** what is hard to change or review today
|
||||
4. **Target seam:** what boundary should become clearer
|
||||
5. **Non-goals:** what you will not clean up in this pass
|
||||
|
||||
If you cannot write those down, the refactor scope is probably too vague.
|
||||
|
||||
## Quick seam review
|
||||
|
||||
Ask these questions before extracting or splitting anything:
|
||||
|
||||
- What is changing together today?
|
||||
- What should be able to change independently?
|
||||
- Does the current API expose mechanics instead of intent?
|
||||
- Does the caller need too much knowledge to use this correctly?
|
||||
- Is this logic actually a policy, model operation, service, task, or module?
|
||||
- Would three likely variations still fit this boundary?
|
||||
|
||||
## Refactor moves by shape
|
||||
|
||||
### Extract a policy when
|
||||
|
||||
- the code is making a pure business decision
|
||||
- the workflow contains business `if` logic
|
||||
- you can test it with plain values
|
||||
|
||||
### Extract a model operation when
|
||||
|
||||
- the code is applying an already-approved event
|
||||
- the work is deterministic state math
|
||||
- the workflow is mixing decision and state transition logic
|
||||
|
||||
### Extract a task when
|
||||
|
||||
- one workflow step has a stable intention name
|
||||
- the parent workflow is becoming hard to read
|
||||
- the step has its own scenario surface
|
||||
|
||||
### Extract a module when
|
||||
|
||||
- a cluster of tasks, policies, and services forms one capability
|
||||
- you want a smaller public API and hidden internals
|
||||
- multiple callers should depend on a shared intent boundary
|
||||
|
||||
### Keep code inline when
|
||||
|
||||
- the logic is still trivial
|
||||
- the boundary has no stable meaning yet
|
||||
- extraction would increase pass-through plumbing without reducing complexity
|
||||
|
||||
## Safe execution pattern
|
||||
|
||||
1. Add or tighten a test at the public seam.
|
||||
2. Make one structural move.
|
||||
3. Run verification.
|
||||
4. Stop if behavior becomes unclear.
|
||||
5. Repeat until the target seam is real and reviewable.
|
||||
|
||||
Prefer many small green moves over one heroic rewrite.
|
||||
|
||||
## What good looks like
|
||||
|
||||
A good refactor leaves behind:
|
||||
|
||||
- a clearer intention-based API
|
||||
- less caller knowledge required
|
||||
- less hidden authority
|
||||
- stronger locality for future changes
|
||||
- a smaller review surface for humans and LLMs
|
||||
|
||||
## What to avoid
|
||||
|
||||
- extracting wrappers that only rename plumbing
|
||||
- introducing modules before there is real pressure
|
||||
- keeping broad god-interfaces for convenience
|
||||
- mixing business decisions into services
|
||||
- using refactoring as an excuse to redesign unrelated areas
|
||||
@@ -0,0 +1,107 @@
|
||||
# How to Review an LLM-Generated Design
|
||||
|
||||
This guide is for the human reviewer.
|
||||
Use it when the LLM has produced a design artifact and you need to decide whether it is safe to accept, revise, or reject.
|
||||
|
||||
## What you are reviewing
|
||||
|
||||
You are not primarily reviewing style.
|
||||
You are reviewing whether the design artifact captures the domain correctly enough that implementation will be mostly mechanical.
|
||||
|
||||
## Recommended review order
|
||||
|
||||
### 1. Read the feature discovery first
|
||||
|
||||
Start with `design/feature/<feature-slug>/discovery.md`.
|
||||
Ignore code and framework details at the start.
|
||||
Ask whether the artifact reflects the real business situation, user intent, candidate bounded contexts, and possible outcomes.
|
||||
|
||||
If the feature story is muddy, everything downstream will be muddy too.
|
||||
|
||||
### 2. Check the decomposition map
|
||||
|
||||
Read `design/feature/<feature-slug>/design.md`.
|
||||
Confirm that:
|
||||
|
||||
- bounded contexts are explicit
|
||||
- feature steps map to workflow slices
|
||||
- cross-context handoffs are recorded
|
||||
- the chosen slice actually belongs to one bounded context
|
||||
|
||||
### 3. Inspect the slice discovery and core sketch
|
||||
|
||||
Read `02-discovery.md` and `03-core-sketch.md` for the selected workflow slice.
|
||||
The policy sketch should reveal the decision boundary.
|
||||
It should be obvious what information is needed to make the decision.
|
||||
|
||||
This is where shallow model output often shows up.
|
||||
If the signature still contains vague blobs like `Data`, `Context`, or `Info`, the design is probably not ready.
|
||||
|
||||
### 4. Inspect the F# blueprint
|
||||
|
||||
Read `04-blueprint.fs`.
|
||||
Look for evidence that the model is encoding business meaning rather than storing everything in generic shapes.
|
||||
|
||||
Ask:
|
||||
|
||||
- Are important lifecycle states explicit?
|
||||
- Are primitives replaced with domain concepts where it matters?
|
||||
- Are invalid combinations harder to express?
|
||||
- Is the naming domain-specific and precise?
|
||||
- Is the slice contract frozen clearly enough that assembly is mechanical?
|
||||
|
||||
### 5. Inspect separation and boundaries
|
||||
|
||||
The final contract should separate concerns clearly:
|
||||
|
||||
- policy for pure decisions
|
||||
- model for pure state transitions
|
||||
- workflow for impure orchestration
|
||||
- feature-level orchestration separate from slice-local decision logic
|
||||
|
||||
If those concerns blur together, the implementation will likely blur too.
|
||||
|
||||
### 6. Compare against the reference example
|
||||
|
||||
Use these reference docs as comparison material:
|
||||
|
||||
- `../tutorials/worked-example-truck-loading.md`
|
||||
- `../reference/design-artifact-template.md`
|
||||
- `../reference/review-checklist.md`
|
||||
- `../explanation/naming-for-domain-modeling.md`
|
||||
|
||||
You are not comparing domain details.
|
||||
You are comparing clarity, shape, and separation of concerns.
|
||||
|
||||
## Red flags
|
||||
|
||||
Common signs that the artifact is not ready:
|
||||
|
||||
- a generic object with a `status` field where separate states should exist
|
||||
- policy outputs like `true` or `false` with no domain fact payload
|
||||
- workflow concerns mixed into the policy
|
||||
- infrastructure types leaking into the design artifact
|
||||
- naming that sounds like programming jargon instead of domain language
|
||||
- a final contract that still feels invented rather than discovered
|
||||
- a refactor proposal that preserves backwards compatibility without an explicit reason, even though the compatibility requirement keeps a worse design in place
|
||||
|
||||
## When to ask the LLM for a revision
|
||||
|
||||
Ask for a revision when the design fails for reasons of shape, not just polish.
|
||||
For example:
|
||||
|
||||
- the command or events are wrong
|
||||
- the state model hides important boundaries
|
||||
- the policy does not expose the needed information
|
||||
- the artifact skips from story to code without freezing the design
|
||||
|
||||
A good revision request is specific.
|
||||
Say what phase is weak and what you want clarified.
|
||||
|
||||
## A practical review question
|
||||
|
||||
A useful test is this:
|
||||
|
||||
> If I handed this artifact to a careful engineer, could they implement it without inventing missing domain meaning?
|
||||
|
||||
If the answer is no, the artifact is not ready.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Project Conventions
|
||||
|
||||
This page is the quick reference for conventions that a human reviewer is likely to check first.
|
||||
For the philosophy behind these conventions, read the explanation docs.
|
||||
|
||||
## Naming
|
||||
|
||||
- Use domain language, not generic programmer words.
|
||||
- Prefer explicit lifecycle states such as `DraftItem` or `ActiveOrder` over one object with a broad `status` field.
|
||||
- Use nouns for stateful objects, past-tense facts for events, and imperative verb phrases for commands.
|
||||
- Maintain a project-wide shared language in `shared-language.md`.
|
||||
- Maintain bounded-context shared language in `design/workflows/<bounded-context-slug>/shared-language.md`.
|
||||
- Record feature-level naming decisions in `design/feature/<feature-slug>/discovery.md` and slice-level naming decisions in the workflow artifacts.
|
||||
- See `naming-lexicon.md`, `shared-language.md`, and `../explanation/naming-for-domain-modeling.md` for guidance.
|
||||
|
||||
## Design artifact shape
|
||||
|
||||
A complete operational design set now usually includes:
|
||||
|
||||
- feature discovery in `design/feature/<feature-slug>/discovery.md`
|
||||
- feature decomposition in `design/feature/<feature-slug>/design.md`
|
||||
- bounded-context shared language in `design/workflows/<bounded-context-slug>/shared-language.md`
|
||||
- slice discovery, core sketch, and F# blueprint under `design/workflows/<bounded-context-slug>/<workflow-slug>/`
|
||||
- status tracking in `design/feature/<feature-slug>/status.md`
|
||||
|
||||
Use `design-artifact-template.md` for by-hand practice and `design-artifact-structure.md` for the repository artifact layout.
|
||||
|
||||
## Documentation organization
|
||||
|
||||
This repo uses a modified Diátaxis structure:
|
||||
|
||||
- `docs/tutorials/` for learning by example and practice
|
||||
- `docs/how-to/` for specific tasks such as reviewing LLM output
|
||||
- `docs/explanation/` for philosophy and architectural reasoning
|
||||
- `docs/reference/` for templates, checklists, and stable lookup material
|
||||
|
||||
## Code structure pointers
|
||||
|
||||
- `src/contexts/` is the default home for business code.
|
||||
- Each context keeps its own models, policies, workflows, translators, adapters, and other seams as needed.
|
||||
- `src/workflows/` is reserved for top-level workflows that coordinate multiple contexts.
|
||||
- `src/shared/` should contain only tiny ubiquitous primitives and low-meaning technical building blocks.
|
||||
|
||||
## Module boundaries
|
||||
|
||||
- Cross-context imports should go through the target context's public API.
|
||||
- Do not import another context's internal implementation files.
|
||||
- When data crosses context boundaries, prefer translation at the edge over sharing one rich domain type everywhere.
|
||||
|
||||
## Decision preference
|
||||
|
||||
- Prefer correctness, simplicity, and elegance over backwards compatibility.
|
||||
- If compatibility would preserve a worse design, require an explicit reason before keeping it.
|
||||
- Prefer combined local logic by default during initial implementation.
|
||||
- Extract a new layer, module, or helper only when it materially improves nameability, testability, reuse, boundary clarity, or duplicated complexity.
|
||||
- Good names can expose a useful seam, but naming alone does not justify extra indirection.
|
||||
|
||||
## Review reminder
|
||||
|
||||
When in doubt, compare the artifact being reviewed against:
|
||||
|
||||
- `../tutorials/worked-example-truck-loading.md`
|
||||
- `review-checklist.md`
|
||||
- `design-artifact-template.md`
|
||||
@@ -0,0 +1,143 @@
|
||||
# Design Artifact Structure
|
||||
|
||||
This is the current artifact layout for real feature work.
|
||||
Use this structure for feature discovery, bounded-context decomposition, workflow-slice design, implementation tracking, and handoff between agents or humans.
|
||||
|
||||
## Top-level idea
|
||||
|
||||
- **Feature discovery** happens once per feature.
|
||||
- **Decomposition** maps the feature into bounded contexts and workflow slices.
|
||||
- **Slice design and implementation** happen one workflow slice at a time.
|
||||
- **F# blueprint artifacts** are the frozen contract that implementation translates from.
|
||||
|
||||
## Feature-level artifacts
|
||||
|
||||
Store feature-level documents in:
|
||||
|
||||
```text
|
||||
design/feature/<feature-slug>/
|
||||
├── discovery.md
|
||||
├── design.md
|
||||
└── status.md
|
||||
```
|
||||
|
||||
### `discovery.md`
|
||||
|
||||
Capture:
|
||||
|
||||
- feature story
|
||||
- commands and events at feature level
|
||||
- business rules and invariants at feature level
|
||||
- edge cases at feature level
|
||||
- candidate bounded contexts
|
||||
- candidate workflow slices
|
||||
- feature-level shared language notes
|
||||
|
||||
### `design.md`
|
||||
|
||||
Capture:
|
||||
|
||||
- bounded context inventory
|
||||
- feature-step to workflow-slice map
|
||||
- cross-context handoffs
|
||||
- recommended slice order
|
||||
- orchestration notes
|
||||
- open questions
|
||||
|
||||
### `status.md`
|
||||
|
||||
Track:
|
||||
|
||||
- current feature phase
|
||||
- current workflow slice
|
||||
- decomposition completion
|
||||
- per-slice gate status
|
||||
- blockers and handoff notes
|
||||
|
||||
Use [design-status-template.md](design-status-template.md) as the base template.
|
||||
|
||||
## Workflow-level artifacts
|
||||
|
||||
Store workflow-slice documents in:
|
||||
|
||||
```text
|
||||
design/workflows/<bounded-context-slug>/
|
||||
├── shared-language.md
|
||||
├── shared-model.fs # optional, when the context has reusable F# domain pieces
|
||||
└── <workflow-slug>/
|
||||
├── 01-decomposition.md
|
||||
├── 02-discovery.md
|
||||
├── 03-core-sketch.md
|
||||
└── 04-blueprint.fs
|
||||
```
|
||||
|
||||
### `shared-language.md`
|
||||
|
||||
Capture bounded-context-local ubiquitous language:
|
||||
|
||||
- what the context owns
|
||||
- preferred terms
|
||||
- rejected synonyms
|
||||
- notes about overlap with project-wide language in [shared-language.md](shared-language.md)
|
||||
|
||||
### `01-decomposition.md`
|
||||
|
||||
Capture:
|
||||
|
||||
- owning bounded context
|
||||
- trigger
|
||||
- purpose and success outcome
|
||||
- inputs owned by this context
|
||||
- observed inputs from other contexts
|
||||
- downstream handoffs
|
||||
- dependencies on other slices
|
||||
- explicit note that decision logic stays inside the owning context
|
||||
|
||||
### `02-discovery.md`
|
||||
|
||||
Capture:
|
||||
|
||||
- happy path
|
||||
- edge cases
|
||||
- business rules and invariants
|
||||
- decisions owned by this context
|
||||
- handoff assumptions
|
||||
- open questions
|
||||
|
||||
### `03-core-sketch.md`
|
||||
|
||||
Capture:
|
||||
|
||||
- command
|
||||
- required owned state
|
||||
- observed external inputs
|
||||
- pseudo policy signature
|
||||
- events
|
||||
- boundary notes about what remains feature-level orchestration
|
||||
|
||||
### `04-blueprint.fs`
|
||||
|
||||
This is the frozen F# contract for one workflow slice.
|
||||
It should include:
|
||||
|
||||
- domain primitives
|
||||
- commands
|
||||
- events
|
||||
- states / aggregates
|
||||
- `decide`
|
||||
- `apply`
|
||||
- workflow contract signature
|
||||
|
||||
## Rules of use
|
||||
|
||||
- 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.
|
||||
- Implementation should proceed one slice at a time from the frozen F# blueprint.
|
||||
|
||||
## Relationship to the by-hand docs
|
||||
|
||||
The [../by-hand/README.md](../by-hand/README.md) material keeps the simpler 5-phase human practice flow.
|
||||
Use that to build judgment.
|
||||
Use this page for the actual repository artifact structure.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Design Artifact Template
|
||||
|
||||
Use this template when drafting a design manually or when asking an LLM to produce a design artifact for review.
|
||||
|
||||
## 1. Story
|
||||
|
||||
Write the business situation in plain language.
|
||||
|
||||
```text
|
||||
A <role> attempts to <action>.
|
||||
If <condition>, it succeeds.
|
||||
If <condition>, it fails.
|
||||
```
|
||||
|
||||
## 2. Event storming
|
||||
|
||||
### Command
|
||||
|
||||
```text
|
||||
<ImperativeVerbNoun>
|
||||
```
|
||||
|
||||
### Timeline
|
||||
|
||||
1. <user intent>
|
||||
2. <business evaluation>
|
||||
3. <success or failure fact>
|
||||
|
||||
### Events
|
||||
|
||||
```text
|
||||
<SuccessEvent>
|
||||
<FailureEvent>
|
||||
```
|
||||
|
||||
## 3. Core sketch
|
||||
|
||||
Write the first policy sketch with rough concepts if needed.
|
||||
|
||||
```fsharp
|
||||
decide : NeededInput -> NeededState -> Result<SuccessEvent, FailureEvent>
|
||||
```
|
||||
|
||||
Then rewrite it using domain-specific concepts.
|
||||
|
||||
## 4. Domain model
|
||||
|
||||
### Primitives
|
||||
|
||||
```fsharp
|
||||
type ExampleId = string
|
||||
```
|
||||
|
||||
### Compound objects
|
||||
|
||||
```fsharp
|
||||
type Example = {
|
||||
Id: ExampleId
|
||||
}
|
||||
```
|
||||
|
||||
### State variants
|
||||
|
||||
```fsharp
|
||||
type ActiveExample = { Id: ExampleId }
|
||||
type ArchivedExample = { Id: ExampleId }
|
||||
|
||||
type ExampleState =
|
||||
| Active of ActiveExample
|
||||
| Archived of ArchivedExample
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
```fsharp
|
||||
type ExampleSucceeded = { Id: ExampleId }
|
||||
type ExampleRejected = { Reason: RejectionReason }
|
||||
```
|
||||
|
||||
## 5. Final contract
|
||||
|
||||
```fsharp
|
||||
decide : Input -> State -> Result<SuccessEvent, FailureEvent>
|
||||
apply : State -> SuccessEvent -> State
|
||||
workflow : InputId -> Effect<Response>
|
||||
```
|
||||
|
||||
## 6. Shared language and naming decisions
|
||||
|
||||
Record the canonical words for this feature so design and code stay consistent. If a term is project-wide rather than feature-local, also add it to `docs/reference/shared-language.md`.
|
||||
|
||||
### Canonical terms
|
||||
|
||||
- `<Preferred term>`: short definition
|
||||
- `<Preferred term>`: short definition
|
||||
|
||||
### Rejected synonyms
|
||||
|
||||
- Use `<Preferred term>`, not `<Rejected term>`
|
||||
- Use `<Preferred term>`, not `<Rejected term>`
|
||||
|
||||
### Naming grammar for this feature
|
||||
|
||||
- Commands use: `<ImperativeVerbNoun>`
|
||||
- Events use: `<PastTenseFact>`
|
||||
- States / objects use: `<DomainNoun>`
|
||||
- Workflow names use: `<verbNoun>`
|
||||
|
||||
## 7. Reviewer notes
|
||||
|
||||
Document any assumptions, open domain questions, or naming choices the reviewer should inspect.
|
||||
@@ -0,0 +1,117 @@
|
||||
# Design Status Template
|
||||
|
||||
Use this file as `design/feature/<feature-slug>/status.md`.
|
||||
It tracks feature-level discovery/decomposition, workflow slices, and per-slice progress through the downstream pipeline.
|
||||
|
||||
## Feature
|
||||
|
||||
- Name: `<feature-name>`
|
||||
- Feature slug: `<feature-slug>`
|
||||
- Current phase: `Feature Discovery | Context & Workflow Decomposition | Slice Discovery | Core Sketch | Blueprint | Design Security Review | Assembly | Implementation Security Review | Refactor Diagnosis | Refactor Execution | Done`
|
||||
- Overall status: `In Progress | Blocked | Ready for Next Phase | Complete`
|
||||
- Security verification status: `Not Started | Design Review Needed | Design Review Complete | Implementation Review Needed | Implementation Review Complete`
|
||||
- Current workflow slice: `<bounded-context-slug>/<workflow-slug> | none`
|
||||
|
||||
## Feature Artifacts
|
||||
|
||||
- [ ] `design/feature/<feature-slug>/discovery.md`
|
||||
- [ ] `design/feature/<feature-slug>/design.md`
|
||||
- [ ] `design/feature/<feature-slug>/status.md`
|
||||
|
||||
## Feature Discovery Gate
|
||||
|
||||
- [ ] feature goal and actor intents captured
|
||||
- [ ] commands and events identified at feature level
|
||||
- [ ] business rules and invariants captured at feature level
|
||||
- [ ] edge cases captured at feature level
|
||||
- [ ] candidate bounded contexts identified
|
||||
- [ ] candidate workflow inventory identified
|
||||
- [ ] project-wide shared-language updates captured
|
||||
- [ ] approved for context and workflow decomposition
|
||||
|
||||
## Context & Workflow Decomposition Gate
|
||||
|
||||
- [ ] bounded contexts confirmed
|
||||
- [ ] feature steps mapped to workflow slices
|
||||
- [ ] cross-context handoffs recorded
|
||||
- [ ] per-context shared-language files created or updated
|
||||
- [ ] workflow folders created with `01-decomposition.md`
|
||||
- [ ] recommended slice order recorded
|
||||
- [ ] approved to begin slice discovery
|
||||
|
||||
## Workflow Slice Tracker
|
||||
|
||||
| Bounded Context | Workflow Slice | Slice Discovery | Core Sketch | Blueprint | Design Security | Assembly | Impl Security | Refactor | Notes |
|
||||
| :--------------- | :---------------- | :-------------- | :------------ | :------------ | :-------------- | :------------ | :------------ | :------------ | :------- |
|
||||
| `<context-slug>` | `<workflow-slug>` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `<note>` |
|
||||
| `<context-slug>` | `<workflow-slug>` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `Not Started` | `<note>` |
|
||||
|
||||
## Current Slice Gates
|
||||
|
||||
### Slice Discovery Gate
|
||||
|
||||
- [ ] selected slice named explicitly
|
||||
- [ ] happy path captured
|
||||
- [ ] edge cases captured
|
||||
- [ ] business rules and invariants captured
|
||||
- [ ] handoff assumptions captured
|
||||
- [ ] context shared-language updates captured
|
||||
- [ ] approved for core sketch
|
||||
|
||||
### Core Sketch Gate
|
||||
|
||||
- [ ] required state is explicit
|
||||
- [ ] command and events are explicit
|
||||
- [ ] policy signature is explicit
|
||||
- [ ] slice boundaries are explicit
|
||||
- [ ] no cross-context decision logic inside the slice
|
||||
- [ ] approved for blueprint
|
||||
|
||||
### Blueprint Gate
|
||||
|
||||
- [ ] domain types make illegal states harder to express
|
||||
- [ ] shared concepts reused appropriately
|
||||
- [ ] policy is pure
|
||||
- [ ] reducer/apply shape is explicit
|
||||
- [ ] workflow contract is explicit
|
||||
- [ ] approved for design security review or assembly
|
||||
|
||||
### Design Security Gate
|
||||
|
||||
- [ ] trust boundaries reviewed
|
||||
- [ ] authority and least privilege reviewed
|
||||
- [ ] sink and data-flow risks reviewed
|
||||
- [ ] blocking findings resolved or explicitly accepted
|
||||
- [ ] approved for assembly
|
||||
|
||||
### Assembly Gate
|
||||
|
||||
- [ ] tests added
|
||||
- [ ] implementation completed
|
||||
- [ ] types pass
|
||||
- [ ] tests passing
|
||||
- [ ] effect AST checks run for modified Effect files
|
||||
- [ ] approved for implementation security review or next slice
|
||||
|
||||
### Implementation Security Gate
|
||||
|
||||
- [ ] implementation security review completed or explicitly deferred
|
||||
- [ ] blocking findings resolved or explicitly accepted
|
||||
- [ ] approved for refactor consideration or next slice
|
||||
|
||||
### Refactor Gate
|
||||
|
||||
- [ ] diagnosis completed if structural changes were needed
|
||||
- [ ] execution completed if approved
|
||||
- [ ] verification rerun after refactor
|
||||
- [ ] slice complete
|
||||
|
||||
## Open Questions / Blockers
|
||||
|
||||
- `<question or blocker>`
|
||||
|
||||
## Context Handoff Notes
|
||||
|
||||
- Read first: `<artifact paths>`
|
||||
- Current focus: `<next slice or current gate>`
|
||||
- Do not change: `<frozen decisions or constraints>`
|
||||
@@ -0,0 +1,50 @@
|
||||
# Directory Layout
|
||||
|
||||
This page describes the **target shape** of the repository after the bounded-context shift.
|
||||
The current codebase may still contain older top-level layer folders while the transition is in progress.
|
||||
|
||||
## Preferred top-level structure
|
||||
|
||||
```text
|
||||
src/
|
||||
├── shared/ # tiny ubiquitous primitives only
|
||||
├── workflows/ # top-level cross-context orchestration only
|
||||
├── tasks/ # optional cross-context steps, only if needed
|
||||
└── contexts/ # first-class bounded contexts
|
||||
```
|
||||
|
||||
## Inside a bounded context
|
||||
|
||||
A context is organized around its own language and review seams, not around a mandatory global layer checklist.
|
||||
A context may contain only the folders it actually needs.
|
||||
|
||||
```text
|
||||
src/contexts/
|
||||
└── billing/
|
||||
├── index.ts # public context API / intent entrypoints
|
||||
├── models/ # local rich domain models
|
||||
├── policies/ # local pure decisions
|
||||
├── interfaces/ # internal capability seams for effectful or replaceable collaborators
|
||||
├── workflows/ # local workflow or task logic if helpful
|
||||
├── translators/ # boundary translation to and from other contexts
|
||||
├── adapters/ # concrete implementations owned by this context
|
||||
├── registries/ # optional runtime strategy mapping
|
||||
└── lib/ # local support code with no broader meaning
|
||||
```
|
||||
|
||||
## Layout rules
|
||||
|
||||
- Organize primarily by **bounded context**, not by one global layer tree.
|
||||
- Keep top-level `workflows/` for business processes that coordinate multiple contexts.
|
||||
- Do not let one context reach into another context's internals.
|
||||
- Cross-context access should go through the target context's **public API** and be coordinated by a top-level workflow.
|
||||
- Keep `shared/` tiny. It is for truly ubiquitous primitives and low-meaning technical building blocks, not rich domain models.
|
||||
- Translate data at context boundaries instead of sharing one rich type everywhere.
|
||||
- Start with folders in one package. Split into packages only when the boundary has proven stable enough to justify the extra rigidity.
|
||||
|
||||
## During migration
|
||||
|
||||
Older docs and code may still mention top-level folders like `src/domain/`, `src/policies/`, `src/adapters/`, or `src/registries/`.
|
||||
Treat those as a legacy layout being phased out, not as the preferred structure for new work.
|
||||
|
||||
For the architectural rationale behind this direction, read [../explanation/architecture/bounded-contexts.md](../explanation/architecture/bounded-contexts.md).
|
||||
@@ -0,0 +1,101 @@
|
||||
# Naming Lexicon
|
||||
|
||||
Use this page as a vocabulary prompt when refining domain names.
|
||||
It is not a source of mandatory terms.
|
||||
Its purpose is to help you replace generic programmer language with more precise domain language.
|
||||
|
||||
## Lifecycle and state
|
||||
|
||||
| Concept | Useful terms | Examples |
|
||||
| :--- | :--- | :--- |
|
||||
| Beginning | `Initiate`, `Register`, `Onboard`, `Provision`, `Draft` | `InitiateTransfer`, `ProvisionServer` |
|
||||
| Middle, active | `Active`, `Pending`, `InFlight`, `Outstanding`, `Live`, `Provisional`, `Tentative` | `OutstandingInvoice`, `ProvisionalBooking` |
|
||||
| Middle, paused | `Suspended`, `Dormant`, `Held`, `Frozen` | `AccountFrozen`, `DormantUser` |
|
||||
| Ending, good | `Complete`, `Fulfill`, `Settle`, `Finalize`, `Resolve` | `SettlePayment`, `FulfillOrder` |
|
||||
| Ending, bad | `Fail`, `Reject`, `Decline`, `Abort`, `Revoke`, `Void`, `Expire` | `VoidTransaction`, `JobFailed` |
|
||||
| Ending, neutral | `Archive`, `Retire`, `Conclude`, `Terminate` | `RetireProduct`, `TerminateSession` |
|
||||
|
||||
## Corrections and changes
|
||||
|
||||
| Concept | Useful terms | Examples |
|
||||
| :--- | :--- | :--- |
|
||||
| Making correct | `Rectify`, `Reconcile`, `Adjust`, `Correct`, `Align` | `ReconcileInventory`, `RectifyBalance` |
|
||||
| Difference | `Variance`, `Discrepancy`, `Delta`, `Offset`, `Deviance` | `InventoryVariance`, `PriceDelta` |
|
||||
| Defect | `Anomaly`, `Irregularity`, `Outlier`, `Malformation` | `DetectAnomaly`, `SignalIrregularity` |
|
||||
|
||||
## Data and collections
|
||||
|
||||
| Concept | Useful terms | Examples |
|
||||
| :--- | :--- | :--- |
|
||||
| Subset | `Cohort`, `Segment`, `Batch`, `Cluster` | `UserCohort`, `ProcessBatch` |
|
||||
| Summary | `Digest`, `Manifest`, `Summary`, `Snapshot` | `ShippingManifest`, `DailyDigest` |
|
||||
| Searching | `Locate`, `Query`, `Scan`, `Inspect`, `Retrieve`, `Fetch`, `Filter`, `Sift` | `LocateParcel`, `RetrieveRecords` |
|
||||
| Assigning | `Allocate`, `Assign`, `Designate`, `Map` | `AllocateResources`, `DesignateOwner` |
|
||||
| Communication | `Notify`, `Alert`, `Dispatch`, `Broadcast`, `Deliver` | `DispatchEmail`, `BroadcastEvent` |
|
||||
|
||||
## Verification and security
|
||||
|
||||
| Concept | Useful terms | Examples |
|
||||
| :--- | :--- | :--- |
|
||||
| Checking | `Validate`, `Verify`, `Authenticate`, `Authorize`, `Audit` | `AuthorizePayment`, `AuditLog` |
|
||||
| Allowing | `Grant`, `Permit`, `Enable`, `Whitelist` | `GrantPermission`, `WhitelistIP` |
|
||||
| Blocking | `Deny`, `Restrict`, `Revoke`, `Ban`, `Blacklist` | `DenyEntry`, `RevokeCertificate` |
|
||||
| Subjects | `Actor`, `Principal`, `Subject`, `Resource` | `RequestActor`, `TargetResource` |
|
||||
|
||||
## Time and scheduling
|
||||
|
||||
| Concept | Useful terms | Examples |
|
||||
| :--- | :--- | :--- |
|
||||
| Delaying | `Defer`, `Postpone`, `Reschedule`, `Queue` | `DeferExecution`, `PostponeMeeting` |
|
||||
| Recurring | `Interval`, `Cadence`, `Frequency`, `Period` | `BillingCadence`, `RefreshInterval` |
|
||||
| Deadlines | `Deadline`, `Cutoff`, `Expiry`, `Threshold` | `SubmissionCutoff`, `ExpiryDate` |
|
||||
|
||||
## Financial and transactional
|
||||
|
||||
| Concept | Useful terms | Examples |
|
||||
| :--- | :--- | :--- |
|
||||
| Moving money | `Transfer`, `Remit`, `Disburse`, `Deposit`, `Withdraw` | `DisburseFunds`, `RemitTax` |
|
||||
| Allocating | `Earmark`, `Reserve`, `Allocate`, `Encumber` | `EarmarkFunds`, `EncumberBudget` |
|
||||
| Undoing | `Revert`, `Rollback`, `Compensate`, `Refund` | `CompensateTransaction` |
|
||||
|
||||
## Boundary, intent, and capability prompts
|
||||
|
||||
| Concept | Useful terms | Examples |
|
||||
| :--- | :--- | :--- |
|
||||
| Caller intent | `GetNextTask`, `CompleteTask`, `PlanProgress`, `AdvanceProgress`, `AssignWork` | `getNextTask`, `completeTask`, `advanceStudentProgress` |
|
||||
| Capability and authority | `Read`, `Write`, `Plan`, `Authorize`, `Approve`, `Administer` | `ReadCurriculum`, `WriteCurriculum`, `ApproveEnrollment` |
|
||||
| Boundary meaning | `Projection`, `Snapshot`, `Decision`, `Eligibility`, `Assignment`, `Progress` | `CurriculumSnapshot`, `EnrollmentDecision`, `TaskAssignment` |
|
||||
| Implementation-shaped names to question | `LessonRecord`, `Manager`, `Handler`, `Processor`, `Data`, `Info` | `LessonManager`, `StudentInfo`, `TaskProcessor` |
|
||||
|
||||
## Compare alternatives before settling on a name
|
||||
|
||||
| Naming pressure test | Alternative 1 | Alternative 2 | Alternative 3 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Progression unit | `nextLesson` | `nextAssignableUnit` | `getNextTask` |
|
||||
| Completion action | `markLessonComplete` | `closeProgressUnit` | `completeTask` |
|
||||
| Sequencing module | `lessonSequencing` | `progressUnitPlanner` | `advanceStudentProgress` |
|
||||
|
||||
Use these comparisons to expose hidden assumptions:
|
||||
|
||||
- does the name lock the model to one current concept?
|
||||
- does it express caller intent or internal structure?
|
||||
- is it naming a capability boundary or only an implementation detail?
|
||||
|
||||
## Bounded seam prompts
|
||||
|
||||
| Seam concern | Useful terms | Examples |
|
||||
| :--- | :--- | :--- |
|
||||
| Reviewable contract | `Decision`, `Command`, `Event`, `Assignment`, `Plan`, `Result` | `PlacementDecision`, `AssignTask`, `TaskPlanned` |
|
||||
| Protected invariant | `Eligible`, `Authorized`, `Assignable`, `Settled`, `Balanced` | `EligibleStudent`, `AssignableTask` |
|
||||
| Black-box boundary | `Service`, `Workflow`, `Policy`, `Gateway`, `Repository` | `StudentProgressWorkflow`, `AuthorizationPolicy` |
|
||||
|
||||
At a bounded seam, prefer names that tell the reviewer:
|
||||
|
||||
- what the caller is trying to achieve
|
||||
- what authority is being exercised
|
||||
- what invariant or decision the boundary protects
|
||||
|
||||
## Reminder
|
||||
|
||||
Prefer domain language from the real problem space over any generic lexicon.
|
||||
This page is a prompt, not a substitute for talking to a domain expert.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Project Structure Overview
|
||||
|
||||
This template now treats **bounded contexts as the primary organizing boundary**.
|
||||
Pure vs impure separation still matters, but it now lives mostly **inside each context** instead of as one global folder tree.
|
||||
|
||||
## Key documentation
|
||||
|
||||
- [Directory Layout](directory-layout.md): The preferred context-first folder map.
|
||||
- [Bounded Context Architecture Statement](../explanation/architecture/bounded-contexts.md): The governing architecture direction.
|
||||
- [Architecture Reasoning](../explanation/architecture/index.md): The deeper rationale behind context boundaries, workflow seams, and purity decisions.
|
||||
- [Architecture Rationale Summary](../explanation/architecture/rationale-summary.md): A short refresher for reviews and interviews.
|
||||
- [Conventions](conventions.md): Naming, file organization, and documentation conventions.
|
||||
- [Review Checklist for TDFDDD Artifacts](review-checklist.md): The quickest way to inspect a generated design artifact.
|
||||
|
||||
## Short version
|
||||
|
||||
- `src/contexts/` holds first-class bounded contexts.
|
||||
- `src/workflows/` holds top-level cross-context orchestration.
|
||||
- `src/shared/` stays tiny.
|
||||
- Cross-context calls go through public APIs, not internal file imports.
|
||||
- Rich domain types stay local to their context unless a primitive is truly ubiquitous.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Refactor Checklist
|
||||
|
||||
Use this checklist when reviewing or planning a refactor.
|
||||
|
||||
## Scope
|
||||
|
||||
- Is the refactor scope explicit?
|
||||
- Is preserved behavior stated?
|
||||
- Are non-goals stated?
|
||||
- Is the success boundary clear?
|
||||
|
||||
## Change pressure
|
||||
|
||||
- What is currently changing together?
|
||||
- What should be able to change independently?
|
||||
- Is there evidence of tangling, pass-through plumbing, or repeated branching?
|
||||
- Is the top-level workflow carrying too much coordination detail?
|
||||
|
||||
## Seam quality
|
||||
|
||||
- Is the seam named by intent rather than mechanism?
|
||||
- Does the contract hide mechanics?
|
||||
- Does the caller need less context after the refactor?
|
||||
- Does the seam require narrower authority than before?
|
||||
- Would at least three likely variations still fit the boundary naturally?
|
||||
|
||||
## Correct shape
|
||||
|
||||
- Is pure decision logic in a policy?
|
||||
- Is deterministic state transition logic in a model operation?
|
||||
- Are services limited to capabilities and integration?
|
||||
- Are tasks used for meaningful orchestration steps rather than thin wrappers?
|
||||
- Is a module justified by a real bounded capability?
|
||||
|
||||
## Safety
|
||||
|
||||
- Are there tests or type constraints protecting the public seam?
|
||||
- Is behavior clarified before structure is moved?
|
||||
- Are changes being made in small reviewable steps?
|
||||
|
||||
## Outcome
|
||||
|
||||
- Is preserved behavior still intact?
|
||||
- Is the code easier to explain?
|
||||
- Is review load lower?
|
||||
- Is the new boundary more stable under likely future changes?
|
||||
@@ -0,0 +1,73 @@
|
||||
# Review Checklist for TDFDDD Artifacts
|
||||
|
||||
Use this checklist when reviewing a design produced by a human or an LLM.
|
||||
|
||||
## Feature story and scope
|
||||
|
||||
- Does `design/feature/<feature-slug>/discovery.md` describe a real business action without implementation detail?
|
||||
- Is the user intent clear?
|
||||
- Are candidate bounded contexts and candidate workflows visible?
|
||||
- Are the feature-level outcomes both visible?
|
||||
|
||||
## Decomposition
|
||||
|
||||
- Does `design/feature/<feature-slug>/design.md` map feature steps to workflow slices?
|
||||
- Is each workflow slice owned by one bounded context?
|
||||
- Are cross-context handoffs explicit?
|
||||
- Is the recommended slice order coherent?
|
||||
|
||||
## Slice discovery and core sketch
|
||||
|
||||
- Does `02-discovery.md` capture the happy path, edge cases, and invariants for the slice?
|
||||
- Does `03-core-sketch.md` make the decision boundary obvious?
|
||||
- Is the required owned state explicit?
|
||||
- Are vague placeholders eliminated before the design is frozen?
|
||||
|
||||
## F# blueprint
|
||||
|
||||
- Does `04-blueprint.fs` model important lifecycle states explicitly?
|
||||
- Are illegal combinations harder to express?
|
||||
- Are key primitives wrapped in domain concepts where it matters?
|
||||
- Is the naming precise and domain-oriented?
|
||||
- Is business meaning encoded in the model instead of hidden in generic flags?
|
||||
|
||||
## Contract separation
|
||||
|
||||
- Is the policy pure?
|
||||
- Is the model responsible only for state transition math?
|
||||
- Is the workflow responsible only for orchestration and I/O?
|
||||
- Are infrastructure concerns kept out of the design artifact?
|
||||
- Do cross-module imports go through module public APIs instead of reaching into internal files?
|
||||
|
||||
## Review focus
|
||||
|
||||
- Are the contracts, domain types, policies, workflows, and adapter seams explicit enough to review with high confidence?
|
||||
- Does the architecture keep business meaning concentrated in policies and workflows instead of buried in service internals?
|
||||
- Can service and adapter internals be trusted mostly through seam tests and type constraints rather than line-by-line domain review?
|
||||
- Do service implementations avoid accumulating hidden business logic?
|
||||
|
||||
## Security review readiness
|
||||
|
||||
- Are trust boundaries visible enough for a reviewer to identify where untrusted data enters?
|
||||
- Are parsing boundaries explicit enough to tell where raw input becomes trusted domain types?
|
||||
- Are likely dangerous sinks or sink categories named, even if implementation details come later?
|
||||
- Are capability boundaries explicit enough to spot over-broad authority before assembly?
|
||||
- Are secrets, sensitive config, and external trust assumptions called out where relevant?
|
||||
- Are blast-radius, detection, recovery, and security-testing expectations visible enough to review?
|
||||
|
||||
## Readiness
|
||||
|
||||
- Could an engineer implement this without inventing missing domain meaning?
|
||||
- Could a reviewer explain what is being verified at each phase?
|
||||
- Does the final artifact feel discovered rather than improvised?
|
||||
- When tradeoffs appear, does the result prefer correctness, simplicity, and elegance over backwards compatibility unless compatibility is explicitly required?
|
||||
- For refactors, is backwards compatibility preserved only when there is an explicit reason to keep it, rather than by default?
|
||||
|
||||
## Reference docs
|
||||
|
||||
Compare against:
|
||||
|
||||
- `../tutorials/worked-example-truck-loading.md`
|
||||
- `../reference/design-artifact-template.md`
|
||||
- `../explanation/naming-for-domain-modeling.md`
|
||||
- `../explanation/why-events-not-booleans.md`
|
||||
@@ -0,0 +1,67 @@
|
||||
# Rule Examples
|
||||
|
||||
## Core Rules
|
||||
```markdown
|
||||
- [Primary-No-this] never use `this` keyword
|
||||
```
|
||||
|
||||
## level-1
|
||||
```markdown
|
||||
- [Primary-No-This] Never use `this` keyword
|
||||
❌ `onClick={() => this.handleClick()}`
|
||||
✅ `onClick={() => handleClick()}`
|
||||
```
|
||||
|
||||
## level-2
|
||||
~~~markdown
|
||||
Interfaces use `Context.Tag` to declare dependencies, and layers provide adapter implementations.
|
||||
Keep decisions as pure functions even when they stay local to a module.
|
||||
Do not promote every small decision to `src/policies/`; only extract when it materially improves nameability, testability, reuse, or boundary clarity.
|
||||
|
||||
**❌ WRONG - Do NOT use classes with `this`:**
|
||||
```typescript
|
||||
// DON'T DO THIS - uses 'this' and class-based approach
|
||||
export class UserService extends Effect.Service<UserService>()("UserService", {
|
||||
effect: Effect.gen(function*() {
|
||||
const db = yield* Database
|
||||
|
||||
return {
|
||||
getUser: (id: UserId) => this.db.query(...) // ❌ Uses 'this'
|
||||
}
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**✅ CORRECT - Use Context.Tag and plain objects:**
|
||||
```typescript
|
||||
// Define the service interface
|
||||
export interface FileSystemStorageService {
|
||||
readonly readFile: (path: string) => Effect.Effect<string, GitPersistenceError>
|
||||
readonly writeFile: (path: string, content: string) => Effect.Effect<void, GitPersistenceError>
|
||||
readonly listFiles: (path: string) => Effect.Effect<string[], GitPersistenceError>
|
||||
readonly commit: (message: string) => Effect.Effect<void, GitPersistenceError>
|
||||
}
|
||||
|
||||
// Create the service tag
|
||||
export class FileSystemStorageService extends Context.Tag("FileSystemStorageService")<
|
||||
FileSystemStorageService,
|
||||
FileSystemStorageService
|
||||
>() {
|
||||
static of(impl: FileSystemStorageService): FileSystemStorageService {
|
||||
return impl
|
||||
}
|
||||
}
|
||||
```
|
||||
~~~
|
||||
|
||||
## checklist
|
||||
```markdown
|
||||
|
||||
- 1. [ ] [Primary-No-This] Never use `this` keyword
|
||||
❌ `onClick={() => this.handleClick()}`
|
||||
✅ `onClick={() => handleClick()}`
|
||||
...
|
||||
- 20. [ ] [Composition-Over-Inheritance] Always use composition, never inheritance
|
||||
❌ `interface Node() {}; class ContentNode extends Node {};`
|
||||
✅ ``
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
# Shared Language
|
||||
|
||||
Use this page to keep the repository's ubiquitous language stable.
|
||||
It is the project-wide glossary for terms that should mean one thing everywhere in design artifacts, code, tests, and reviews.
|
||||
|
||||
## Why this exists
|
||||
|
||||
Naming inconsistency creates design inconsistency.
|
||||
When the same concept is called three different things, reviewers and LLMs start inventing distinctions that are not real.
|
||||
A stable shared language improves design quality, review quality, and implementation consistency.
|
||||
|
||||
This file is for **project-wide terms**.
|
||||
Feature-specific naming choices should also be recorded in the relevant design artifact.
|
||||
|
||||
## How to use it
|
||||
|
||||
- Add terms that appear across multiple features or modules.
|
||||
- Prefer short domain definitions over implementation detail.
|
||||
- Record preferred terms and rejected synonyms.
|
||||
- Update this file when the team decides one name should win.
|
||||
- Keep the grammar consistent with the naming rules in `../explanation/naming-for-domain-modeling.md`.
|
||||
|
||||
## Core grammar
|
||||
|
||||
- **Commands**: imperative intents such as `LoadPackage`
|
||||
- **Events**: past-tense facts such as `PackageLoaded`
|
||||
- **States / Objects**: domain nouns such as `LoadingTruck`
|
||||
- **Workflows**: verb-noun names such as `loadPackage`
|
||||
- **Policies**: decision-oriented names such as `decideLoadEligibility`
|
||||
|
||||
## Project-wide glossary template
|
||||
|
||||
| Term | Meaning | Use this, not that | Notes |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `<PreferredTerm>` | `<Short domain meaning>` | `<PreferredTerm>` not `<RejectedSynonym>` | `<Optional note>` |
|
||||
| `<PreferredTerm>` | `<Short domain meaning>` | `<PreferredTerm>` not `<RejectedSynonym>` | `<Optional note>` |
|
||||
|
||||
## Review questions
|
||||
|
||||
- Would a domain expert recognize these terms?
|
||||
- Are the same concepts named consistently across story, events, model, code, and tests?
|
||||
- Are we accidentally using programmer words where domain words should exist?
|
||||
- Have we documented which synonym is preferred when multiple names are plausible?
|
||||
@@ -0,0 +1,448 @@
|
||||
# Agent Pipeline Notes
|
||||
|
||||
## 1. Reality Check
|
||||
|
||||
- Fully automated code writing is close for bounded, low-risk, well-specified work.
|
||||
- Fully unsupervised ownership of large, evolving, high-stakes systems is not close.
|
||||
- Raw coding ability is improving faster than architectural consistency, uncertainty calibration, and trustworthy self-review.
|
||||
- In the near term, the goal is not to remove review entirely. The goal is to move review up a level.
|
||||
|
||||
## 2. Current Bottleneck
|
||||
|
||||
- The early design phases in this repository are already relatively strong.
|
||||
- The main bottleneck is assembly and the review/refactor thrash around assembly.
|
||||
- The biggest time sink is repeated loops around common implementation issues, potential refactors, and reviewing too much low-level detail.
|
||||
- Strong design artifacts reduce the need to reconstruct intent from code, but they do not yet fully remove the need for human judgment.
|
||||
|
||||
## 3. What a Pipeline Adds Beyond Manual Skill Use
|
||||
|
||||
Right now, the human is acting as the scheduler and state machine.
|
||||
A pipeline externalizes that work so it is explicit and enforceable.
|
||||
|
||||
Useful additions that were not as necessary when doing the process manually:
|
||||
|
||||
- machine-checkable approval state
|
||||
- explicit slice definitions
|
||||
- spec-to-code traceability rules
|
||||
- human-signoff criteria by phase
|
||||
- artifact diffs between stages
|
||||
- automatic verification bundles
|
||||
- replayable evaluation runs
|
||||
- thrash/change-war detection
|
||||
- audit trail for decisions and outcomes
|
||||
|
||||
The main benefit is not “agents do more coding.”
|
||||
The main benefit is that the process becomes more stable, repeatable, and reviewable.
|
||||
|
||||
## 4. What to Pipeline First
|
||||
|
||||
Build the thinnest useful pipeline around the current process.
|
||||
Do not start with a large swarm system.
|
||||
|
||||
Best initial targets:
|
||||
|
||||
- phase state machine
|
||||
- artifact and gate manifests
|
||||
- assembly slice runner
|
||||
- verification bundle
|
||||
- thrash detection
|
||||
- small replay benchmark harness
|
||||
|
||||
This should be built as a headless harness first, designed for Argo-style execution rather than an interactive coding CLI.
|
||||
Use existing coding agents and projects like opencode as reference implementations for inner-loop patterns, not as the architectural foundation.
|
||||
|
||||
## 5. What Should Stay Human
|
||||
|
||||
Humans should still own the judgment-heavy transitions:
|
||||
|
||||
- requirements freeze
|
||||
- workflow or slice approval
|
||||
- architecture exceptions
|
||||
- security sign-off for risky changes
|
||||
- final sign-off for high-blast-radius changes
|
||||
- resolving ambiguity or contradictory requirements
|
||||
|
||||
The pipeline should reduce line-by-line review, not eliminate human judgment.
|
||||
|
||||
## 6. Review at a Higher Level
|
||||
|
||||
The practical goal is to move review from low-level code inspection to higher-level conformance review.
|
||||
|
||||
Instead of asking:
|
||||
- What does this code do?
|
||||
- Did the agent miss some implementation detail?
|
||||
|
||||
Try to make review focus on:
|
||||
- Does this slice match the frozen artifact?
|
||||
- Does it violate any domain invariants or trust boundaries?
|
||||
- Did it introduce any risky seams or suspicious shortcuts?
|
||||
- Does it need redesign, human review, or is it safe to merge?
|
||||
|
||||
This is realistic if artifacts become stricter and slices become smaller.
|
||||
|
||||
## 7. Preventing Subtle Logic Errors
|
||||
|
||||
The system will mostly catch errors early rather than prevent every error outright.
|
||||
That is still valuable because catching errors before merge is much cheaper than catching them later.
|
||||
|
||||
Helpful layers:
|
||||
|
||||
- frozen design artifacts
|
||||
- spec-to-code traceability
|
||||
- property and mutation tests where useful
|
||||
- risk-focused seam review
|
||||
- deterministic boundary and architecture checks
|
||||
- forcing agents to cite which invariant each change satisfies
|
||||
|
||||
The point is not perfection.
|
||||
The point is making subtle logic flaws rarer and cheaper to catch.
|
||||
|
||||
## 8. Slices and Assembly Scope
|
||||
|
||||
Assembly often does too much when it translates a whole blueprint at once.
|
||||
A better pattern is:
|
||||
|
||||
1. freeze the artifact
|
||||
2. choose one workflow slice
|
||||
3. implement one tracer-bullet path end to end
|
||||
4. run tests/checks/review
|
||||
5. expand behavior within that slice
|
||||
6. move to the next slice
|
||||
|
||||
A slice is not necessarily one PR per policy or adapter.
|
||||
A slice is one coherent bounded vertical change that may touch a workflow, several policies, and several adapters if they belong to one contract.
|
||||
|
||||
## 9. Cost Reality
|
||||
|
||||
A pipeline can save money only if it reduces retries, review thrash, and change wars.
|
||||
If it creates uncontrolled loops, it can absolutely increase token costs.
|
||||
|
||||
The key metric is not cost per token.
|
||||
The key metric is cost per accepted slice and time saved per accepted slice.
|
||||
|
||||
Important ideas:
|
||||
|
||||
- use expensive reasoning at phase boundaries
|
||||
- use cheaper models only when artifacts are tight and the task is narrow
|
||||
- detect thrash early
|
||||
- benchmark replay on a small representative set, not everything
|
||||
- optimize for time saved, not just token minimization
|
||||
|
||||
If spending more on tokens saves multiple days of work, that can still be a clear win.
|
||||
|
||||
## 10. Minimum Viable Build
|
||||
|
||||
For the clarified goal, LangGraph is worth adopting in the MVP because orchestration, durable execution, and resumable state are now part of the project’s core value.
|
||||
Start with a small headless TypeScript orchestrator built for batch execution inside Argo workflows.
|
||||
Add Langfuse from the beginning so traces, spans, prompts, runs, and outcomes are observable in a way that is useful both operationally and on a resume.
|
||||
|
||||
Suggested MVP pieces:
|
||||
|
||||
- LangGraph workflow graph / state machine for bounded slices
|
||||
- markdown artifacts with small machine-readable frontmatter or JSON manifests
|
||||
- simple TypeScript parsers for artifacts and statuses
|
||||
- Langfuse tracing for each run, stage, model call, and gate decision
|
||||
- verification bundle assembled into one review packet
|
||||
- cheap external thrash-detection heuristics
|
||||
- small replay benchmark harness
|
||||
- one headless executor path suitable for Argo
|
||||
|
||||
This is enough to improve the real workflow and create a strong portfolio project while staying aligned with the desired end-state architecture.
|
||||
|
||||
## 11. Portable Process Core, Disposable Runner
|
||||
|
||||
The right design is a portable process core with a replaceable runner.
|
||||
|
||||
- process rules, states, gates, policies, artifacts, and evaluation logic should be portable
|
||||
- the runtime executor should be disposable or replaceable at the boundary level
|
||||
|
||||
Even if LangGraph is used in the MVP, the value should still live primarily in the process definition, policies, artifacts, event schema, and evaluation logic rather than in LangGraph-specific node wiring.
|
||||
The graph runtime should be treated as an adapter for execution, checkpointing, and resumability, not as the place where domain process knowledge gets trapped.
|
||||
|
||||
## 12. Workflow / Service Boundary for the Pipeline Itself
|
||||
|
||||
Follow the repository’s existing separation when building the pipeline.
|
||||
|
||||
Pure/domain-like pieces:
|
||||
- pipeline state
|
||||
- gate rules
|
||||
- slice metadata
|
||||
- approval rules
|
||||
- conformance decisions
|
||||
|
||||
Workflow/service pieces:
|
||||
- run coding agents
|
||||
- invoke models
|
||||
- read and write artifacts
|
||||
- launch review jobs
|
||||
- open PRs
|
||||
- store logs and run records
|
||||
|
||||
Example:
|
||||
- “Can assembly start?” is a pure policy decision.
|
||||
- “Launch the assembly agent with model X” is a service call inside a workflow.
|
||||
|
||||
That makes the process logic portable and keeps LangGraph as a future adapter instead of a hard dependency.
|
||||
|
||||
## 13. LangGraph: When It Helps
|
||||
|
||||
LangGraph is useful here because orchestration is now part of the main project rather than a future optimization.
|
||||
Its real benefits are:
|
||||
|
||||
- durable execution
|
||||
- resumability
|
||||
- retries
|
||||
- branching workflows
|
||||
- checkpointed state
|
||||
- human-in-the-loop pauses
|
||||
- better operational management for complex graphs
|
||||
- easier alignment with headless batch execution in Argo-style systems
|
||||
|
||||
For this direction, it is reasonable to use LangGraph in the MVP.
|
||||
The main caution is to avoid letting graph node wiring become the only place where business process rules live.
|
||||
Keep process semantics portable even if LangGraph is the first runtime.
|
||||
|
||||
## 14. Training Data Value
|
||||
|
||||
A pipeline can produce much better training data than raw chat logs.
|
||||
The valuable data is not just “the model wrote code.”
|
||||
The valuable data is:
|
||||
|
||||
- given this phase and artifact state
|
||||
- with these checks failing or passing
|
||||
- what action/tool/model choice was correct next
|
||||
- what human correction was needed
|
||||
- what result was ultimately accepted
|
||||
|
||||
This is useful for future model training, replay evaluation, and process improvement.
|
||||
|
||||
## 15. What to Log for Training and Replay
|
||||
|
||||
Do not assume the orchestration engine will automatically create a clean training corpus.
|
||||
Design explicit logging for the data you care about.
|
||||
|
||||
Minimum useful fields:
|
||||
|
||||
- run ID
|
||||
- stage ID
|
||||
- slice ID
|
||||
- artifact versions and hashes
|
||||
- prompt or template version
|
||||
- model choice
|
||||
- full input context given to the model for that step
|
||||
- tool choice and tool arguments
|
||||
- tool outputs or summaries
|
||||
- verification results
|
||||
- gate decision
|
||||
- human corrections or overrides
|
||||
- final disposition (accepted, rejected, redesign, escalated)
|
||||
|
||||
If prompt design or routing rules materially affect cost or quality, log those too.
|
||||
|
||||
## 16. LangGraph vs Training Logs
|
||||
|
||||
LangGraph can persist execution state and checkpoints, and can be useful for debugging.
|
||||
But that is different from having a clean, normalized dataset for:
|
||||
|
||||
- training
|
||||
- replay
|
||||
- evaluation
|
||||
- analytics
|
||||
- cost analysis
|
||||
|
||||
So the orchestration system and the training/event log should be treated as separate concerns.
|
||||
Use orchestration state for execution and recovery.
|
||||
Use explicit event logs for learning and analysis.
|
||||
|
||||
## 17. Hooking Into the Executor Layer
|
||||
|
||||
Start with a headless executor boundary, not with deep integration into an interactive coding CLI.
|
||||
The first goal is not to instrument every internal token.
|
||||
The first goal is to supervise bounded work reliably inside batch execution.
|
||||
|
||||
What the executor boundary should do first:
|
||||
|
||||
- launch a bounded task with a clear artifact bundle
|
||||
- capture the returned summary, diff, and verification results
|
||||
- classify the outcome as continue / needs-human / blocked / failed
|
||||
- stop after one coherent slice or checkpoint
|
||||
- emit structured events and traces to Langfuse
|
||||
|
||||
If an existing coding agent can operate in a non-chatty batch mode for bounded tasks, it can sit behind this executor boundary.
|
||||
If it cannot, keep it for human-guided stages and use a lower-level headless executor for automated assembly later.
|
||||
|
||||
## 18. What to Capture From the Executor
|
||||
|
||||
If possible, capture at least:
|
||||
|
||||
- exact task input sent
|
||||
- artifact bundle provided
|
||||
- model used
|
||||
- tool calls made
|
||||
- tool arguments
|
||||
- result summary
|
||||
- changed files
|
||||
- verification output
|
||||
- whether the run completed, asked for help, or thrashed
|
||||
- trace IDs, run IDs, span metadata, and checkpoint IDs
|
||||
|
||||
For training-grade data, exact per-step context is better than only a final summary.
|
||||
LangGraph persistence and Langfuse traces are helpful, but they are still not the same thing as a clean replay/eval dataset.
|
||||
Explicit event capture is still required.
|
||||
|
||||
## 19. Opencode-Specific Decision Point
|
||||
|
||||
Opencode is now mainly a reference implementation for inner-loop agent behavior rather than the likely runtime foundation.
|
||||
|
||||
Questions to answer before borrowing ideas from it or integrating with it:
|
||||
|
||||
- Which parts are genuinely reusable in a headless Argo-oriented system?
|
||||
- Can its agent loop run cleanly in bounded, low-chatter automation?
|
||||
- Can you capture the effective prompt/context used?
|
||||
- Can you capture tool-use data well enough for replay and training?
|
||||
- Does borrowing from it reduce delivery time more than it increases architectural drag?
|
||||
|
||||
If yes, copy patterns or adapt isolated pieces.
|
||||
If no, then the best path is to keep opencode as a reference and build a smaller dedicated executor for automated assembly and evaluation.
|
||||
|
||||
## 20. Portfolio Value
|
||||
|
||||
This can be a strong AgentOps portfolio project if it is real and measured.
|
||||
The impressive part is not size.
|
||||
The impressive part is demonstrating:
|
||||
|
||||
- staged orchestration
|
||||
- model routing
|
||||
- evaluation and replay
|
||||
- cost and quality tradeoffs
|
||||
- review reduction
|
||||
- guardrails and risk controls
|
||||
- operational judgment about where humans stay in the loop
|
||||
|
||||
A compact, sharp system with metrics is better than a giant “swarm” that is hard to explain.
|
||||
|
||||
## 21. Practical Next Step
|
||||
|
||||
Build a thin layer around the current repository process and use it on real work.
|
||||
Do not pause everything to build a big system.
|
||||
|
||||
A good first direction:
|
||||
|
||||
1. define machine-checkable phase and approval states
|
||||
2. define slice metadata and human sign-off criteria
|
||||
3. implement a small LangGraph state graph for one bounded slice
|
||||
4. wrap verification into one bundle
|
||||
5. add basic thrash detection and Langfuse tracing
|
||||
6. run it on one live workflow inside a headless batch path
|
||||
7. measure review time, retries, accepted-slice cost, and trace quality
|
||||
8. add explicit event logging for replay and evals
|
||||
9. decide whether any existing coding agent is good enough behind the executor boundary
|
||||
10. iterate based on real pain
|
||||
|
||||
## 22. Six-Step Build Order for the First Effect-Template Use Case
|
||||
|
||||
For the first use case, the target is developing software inside this repository structure rather than supporting a fully general coding environment from day one.
|
||||
The build order should stay tightly coupled to the minimum tool surface needed for one bounded workflow slice.
|
||||
|
||||
### Step 1: Prove the End-to-End Slice Loop
|
||||
Goal:
|
||||
- get one bounded software-development slice running end to end in the effect-template style
|
||||
|
||||
Tools to add:
|
||||
- read_file
|
||||
- list_files
|
||||
- write_file
|
||||
- edit_file
|
||||
- run_tests
|
||||
|
||||
Why this first:
|
||||
- proves the basic harness can take a task, operate on repository artifacts, make code changes, run verification, and return a classified result
|
||||
- keeps the tool surface small enough to debug failures clearly
|
||||
|
||||
### Step 2: Add First-Class Observability
|
||||
Goal:
|
||||
- make each run inspectable so failures and bottlenecks are visible immediately
|
||||
|
||||
Tools to add:
|
||||
- trace_run or equivalent Langfuse instrumentation hooks
|
||||
|
||||
Why now:
|
||||
- once the first loop works, observability becomes the fastest way to learn from real runs
|
||||
- tracing should come before adding much more autonomy so failures do not become opaque
|
||||
|
||||
### Step 3: Add Replay and Evaluation Foundations
|
||||
Goal:
|
||||
- make runs reproducible and measurable rather than anecdotal
|
||||
|
||||
Tools to add:
|
||||
- save_replay_record
|
||||
- replay_run
|
||||
|
||||
Why now:
|
||||
- replayability is one of the core differentiators of the harness
|
||||
- this creates a basis for later evals, cost analysis, and regression checks
|
||||
|
||||
### Step 4: Add Gates and Thrash Control
|
||||
Goal:
|
||||
- keep the system from looping uselessly or pushing low-quality output forward
|
||||
|
||||
Tools to add:
|
||||
- gate_evaluator
|
||||
- thrash_detector
|
||||
|
||||
Why now:
|
||||
- after replay exists, it becomes easier to define and tune failure heuristics
|
||||
- this is where the harness starts protecting time and token spend rather than only executing work
|
||||
|
||||
### Step 5: Improve Execution Safety
|
||||
Goal:
|
||||
- make automated runs safer and more diagnosable in headless environments
|
||||
|
||||
Tools to add:
|
||||
- safer shell or sandbox executor
|
||||
- git_diff
|
||||
|
||||
Why now:
|
||||
- once the harness can already complete bounded slices, safety and change inspection become more valuable than adding more raw capability
|
||||
- diff visibility is especially useful for higher-level review
|
||||
|
||||
### Step 6: Add Richer Orchestration
|
||||
Goal:
|
||||
- move from one bounded slice to more capable autonomous workflow behavior
|
||||
|
||||
Tools to add:
|
||||
- planning or decomposition tool
|
||||
- multi-agent delegation or subtask dispatch
|
||||
|
||||
Why last:
|
||||
- richer orchestration compounds complexity quickly
|
||||
- it should be added only after the basic loop, observability, replay, and control mechanisms already work
|
||||
|
||||
## 23. Open Decisions Still Worth Discussing
|
||||
|
||||
Before implementing too much, it would be useful to decide:
|
||||
|
||||
- what exact human approvals are required at each phase
|
||||
- how small slices should be in practice
|
||||
- what counts as thrash vs healthy iteration
|
||||
- what minimum event schema is worth storing now
|
||||
- whether to store full prompts or prompt templates plus resolved context
|
||||
- whether to store full tool outputs or normalized summaries plus raw attachments
|
||||
- how much of the coding CLI can be wrapped without losing its advantages
|
||||
- whether automated assembly should use the same executor as human-guided work
|
||||
- what metrics will prove the pipeline is saving time rather than adding ceremony
|
||||
- what data retention and privacy rules apply if you later train on this data
|
||||
|
||||
## 24. Bottom Line
|
||||
|
||||
The opportunity is real.
|
||||
The trust barrier is still the main constraint.
|
||||
|
||||
So the right move is:
|
||||
- do not wait for models to become magically trustworthy
|
||||
- do not overbuild a giant pipeline first
|
||||
- formalize the process you already use
|
||||
- move review up a level
|
||||
- keep human judgment at the risky seams
|
||||
- log the process in a training- and replay-friendly way
|
||||
- measure whether the pipeline actually saves time
|
||||
@@ -0,0 +1,83 @@
|
||||
# System Prompt: Evolutionary Architecture Pipeline Implementation
|
||||
|
||||
**Goal:** Build a custom CI/CD script that combines spatial data (where code lives), temporal data (when code changes), and structural data (what the code does) to guide incremental refactoring in a TypeScript + Effect.ts codebase.
|
||||
|
||||
**Tech Stack:**
|
||||
1. **Dependency-Cruiser:** To map spatial boundaries (Seams).
|
||||
2. **Hercules:** To map temporal evolutionary coupling.
|
||||
3. **Opengrep:** To identify data-flow anti-patterns (Tramp Data, Branching).
|
||||
4. **TypeScript (Node.js):** The "glue" script (`refactor-bot.ts`) that synthesizes the data.
|
||||
|
||||
---
|
||||
|
||||
### Step 1: Establish Spatial Boundaries (Dependency-Cruiser)
|
||||
**Agent Instructions:**
|
||||
1. Install dependency-cruiser: `npm install -D dependency-cruiser`
|
||||
2. Initialize it `npx depcruise --init` and configure it for a TypeScript environment.
|
||||
3. Modify the `.dependency-cruiser.js` configuration to define "Seams". Group the application by folders (e.g., Domain boundaries, Layers, or Effect modules).
|
||||
4. Create an npm script named `"map:boundaries"` that runs depcruise and outputs the architecture to a JSON file: `npx depcruise src --output-type json > ./.architecture/seams.json`.
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Establish Temporal Coupling (Hercules / Git Mining)
|
||||
**Agent Instructions:**
|
||||
1. Set up a tool to extract **Logical Coupling** from the git history.
|
||||
*(Note for Agent: Use the `src-d/hercules` binary via Docker or Go, OR use the simpler `code-maat` Python/Clojure alternative if Hercules is blocked by local environment constraints).*
|
||||
2. Execute a git log command to get history:
|
||||
```bash
|
||||
git log --all --numstat --date=short --pretty=format:'--%h--%ad--%aN' --no-renames > ./.architecture/logfile.log
|
||||
```
|
||||
3. Run the coupling analysis tool against this log. The goal is to output a `.csv` or `.json` file (`coupling.json`) with three columns/fields: `[FileA, FileB, CoChangePercentage]`.
|
||||
4. Ensure this output is saved to `/.architecture/coupling.json`.
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Implement Deep Static Analysis (Opengrep / Semgrep)
|
||||
**Agent Instructions:**
|
||||
1. Install the open-source CLI (e.g., `npm install -g opengrep` or use `pip install semgrep`).
|
||||
2. Create a folder `/.architecture/rules/`.
|
||||
3. Write custom YAML rules to detect code smells specific to **Effect.ts** and functional pipelines.
|
||||
4. **Rule 1: Tramp Data in Pipelines.** Write a rule that looks for a variable passed into a `.pipe()` or `Effect.gen` that traverses across a recognized boundary un-mutated. Use the ellipsis operator (`...`).
|
||||
*Example concept:*
|
||||
```yaml
|
||||
rules:
|
||||
- id: effect-tramp-data
|
||||
pattern: pipe(..., $MOD_A.get($DATA), ..., $MOD_B.save($DATA), ...)
|
||||
message: "Potential tramp data crossing seam boundaries."
|
||||
languages: [typescript]
|
||||
severity: WARNING
|
||||
```
|
||||
5. Ensure there is a command to run this and output to JSON: `opengrep --config ./.architecture/rules/ --json > ./.architecture/smells.json`
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Write the Synthesis Script (`refactor-bot.ts`)
|
||||
**Agent Instructions:**
|
||||
Write a Node.js TypeScript file (`scripts/refactor-bot.ts`) that acts as the brain. This script must:
|
||||
|
||||
**1. Load the Data:**
|
||||
* Parse `seams.json` (Which files belong to which boundary).
|
||||
* Parse `coupling.json` (How often files change together).
|
||||
* Parse `smells.json` (Where the AST anti-patterns are).
|
||||
|
||||
**2. Detect Cross-Seam Leaks (High External Coupling):**
|
||||
* Look at pairs in `coupling.json` that co-change > 60% of the time, but live in *different* Seams according to `seams.json`.
|
||||
* *Action:* Flag these as **Architectural Leaks**. Check if `smells.json` found tramp data in these specific files.
|
||||
|
||||
**3. Detect Splittable Contexts (Low Internal Cohesion):**
|
||||
* Look at files that exist in the *same* Seam (e.g., both are in `src/payments/`), but have a co-change frequency of < 5%.
|
||||
* *Action:* Flag this boundary as a candidate to be split into two smaller bounded contexts.
|
||||
|
||||
**4. Generate the Output/Report:**
|
||||
* Output a Markdown report summarizing:
|
||||
* **⚠️ High Priority Refactors:** Cross-layer tightly coupled files (includes Semgrep AST context).
|
||||
* **✂️ Suggested Seam Splits:** Modules with low internal cohesion.
|
||||
* Exit with code `1` if the max coupling threshold is breached to fail the CI/CD pipeline, otherwise exit with `0`.
|
||||
|
||||
---
|
||||
|
||||
### Definition of Done for Agent:
|
||||
1. `dependency-cruiser` is configured and outputs `seams.json`.
|
||||
2. Git history pipeline outputs `coupling.json`.
|
||||
3. Opengrep/Semgrep YAML rules are written for Effect.ts and output `smells.json`.
|
||||
4. `refactor-bot.ts` successfully reads all three files and prints an Evolutionary Architecture Report in the console.
|
||||
@@ -0,0 +1,5 @@
|
||||
the "domain-modeling.md" in agent core rules is way too long. should be in docs not filling agent context
|
||||
|
||||
add notes for how agent should name commits. only makes commits after I review the code
|
||||
|
||||
intra bounded contexts should be dry but different bounded contexts can implement the same function
|
||||
@@ -0,0 +1,40 @@
|
||||
capability based use, not passing in the entire db
|
||||
- see wlaschin's presentations on capability based code
|
||||
|
||||
label input as trusted or not, strings are no longer raw
|
||||
|
||||
sanitization notes add trusted vs untrusted string input? ie save string source?
|
||||
|
||||
are the security boundaries comprehensive enough? with 12 concerns I worry that is too many for one agent, I guess you can just keep doing security over and over again. do we cover enough concerns?
|
||||
add sanitization to strings
|
||||
mention seams from legacy code book
|
||||
make sure tests are property tests
|
||||
tdd for domain objects as well
|
||||
|
||||
sub-agent / orchestration documentation ideas:
|
||||
- add shared orchestrator policy for scope control, restart criteria, and limits on agent freedom
|
||||
- add a formal failure taxonomy for agent runs, including drift, tool misuse, silent failure, and recovery notes
|
||||
- add evaluation thresholds and acceptance-check guidance for agent-produced work
|
||||
- add model-routing and cost rules for choosing cheaper vs stronger models
|
||||
- add escalation and recovery policies for when an agent should stop, hand off, or request a fresh context
|
||||
- add guidance for refactors: preserve backwards compatibility only with an explicit reason, otherwise prefer the direct change
|
||||
- add implementation guidance for future sub-agent benchmarks using real TypeScript tests as verification
|
||||
- add lightweight evidence-capture guidance so agent runs can be reviewed later without heavy documentation overhead
|
||||
|
||||
|
||||
# bounded seam identification tool:
|
||||
it feels like you could have a tool that automatically identifies seams (ie seams are pretty obvious in this architecture right? they are layers in this template?) so you could have a tool that automatically traverses the changes so you can see what parts frequently need coordinated edits across seams? ie you could have this run every pr and then once there are places that get enough of a score you could start a refactor flow? or if there are pieces within a seam that always change independently then maybe they should be 2 bounded seams?
|
||||
And yes, your coordinated-change idea is strong. This repo’s explicit layers help, but seams are not just layers; they are places where behavior can vary independently. A tool could mine PRs/commits for:
|
||||
- files/functions frequently changed together
|
||||
- edits that cross module APIs
|
||||
- params/data passed through unchanged across many callers
|
||||
- repeated branching at the same boundary
|
||||
|
||||
# need find bounded contexts step of feature
|
||||
need to find if there are new or existing bounded contexts. arrange repo by bounded context with shared primitives
|
||||
|
||||
# do we have decompose feature into several workflows/tasks skill?
|
||||
define the difference between workflows and tasks
|
||||
|
||||
# need process for refactoring
|
||||
ie I don't need to wait for the template to be done to use it I can just add the improvements later
|
||||
@@ -0,0 +1,4 @@
|
||||
# ideas
|
||||
- use sub package.json to isolate bounded contexts
|
||||
- auto update and refactor, renovate updates dependencies, codemod cli does the refactor
|
||||
- use sourcebot or cocoindex-code to look at the semantic meaning and check if there are similar functions that can be removed. ie "write everything twice" (wet) for projects, "don't repeat yourself" (dry) in bounded contexts. would need to write own pipeline
|
||||
@@ -0,0 +1,637 @@
|
||||
# Effect-TS Best Practices Guide
|
||||
|
||||
This guide documents best practices for writing Effect-TS code, following principles from:
|
||||
- **"Grokking Simplicity"** by Eric Normand (functional programming fundamentals)
|
||||
- **"Domain Modeling Made Functional"** by Scott Wlaschin (domain-driven design)
|
||||
- **Effect-TS patterns** (modern TypeScript effect systems)
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Domain-Driven Design (Scott Wlaschin)
|
||||
|
||||
#### Make Illegal States Unrepresentable
|
||||
|
||||
Use the type system to prevent invalid domain states:
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Primitives allow invalid states
|
||||
import {CpuCount} from "./VmSpec";
|
||||
|
||||
interface VmConfig {
|
||||
cpus: number // Could be 0, negative, or 1000
|
||||
memory: number // Could be any number
|
||||
status: string // Could be typo: "runing"
|
||||
}
|
||||
|
||||
// ✅ GOOD: Types enforce domain rules
|
||||
const Slug = Schema.String.pipe(
|
||||
Schema.pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
|
||||
message: () =>
|
||||
"Slug must be lowercase letters, numbers, and hyphens only (e.g., 'my-snippet-name')",
|
||||
}),
|
||||
Schema.minLength(1),
|
||||
Schema.maxLength(100),
|
||||
Schema.brand('Slug'),
|
||||
);
|
||||
export type Slug = typeof Slug.Type;
|
||||
|
||||
|
||||
export const CpuCount = Schema.Number.pipe(
|
||||
Schema.int(),
|
||||
Schema.between(1, 64), // Physical constraint
|
||||
Schema.brand("CpuCountBrand" as const)
|
||||
)
|
||||
// Make sure to bind type
|
||||
export type CpuCount = typeof CpuCount.Type;
|
||||
|
||||
export const VmStatus = Schema.Literal("running", "stopped", "error").pipe(
|
||||
Schema.brand("VmStatus")
|
||||
)
|
||||
export type VmStatus = typeof VmStatus.Type
|
||||
|
||||
interface VmConfig {
|
||||
slug: Slug
|
||||
cpus: CpuCount // Guaranteed valid
|
||||
memory: MemoryMB // Guaranteed valid
|
||||
status: VmStatus // Typos impossible
|
||||
}
|
||||
```
|
||||
|
||||
#### Model the Domain Precisely
|
||||
|
||||
Domain types should exactly match business concepts:
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Generic, imprecise
|
||||
type NetworkConfig = {
|
||||
vlan: number
|
||||
subnet: string
|
||||
}
|
||||
|
||||
// ✅ GOOD: Precise domain model
|
||||
export const VlanId = Schema.Number.pipe(
|
||||
Schema.int(),
|
||||
Schema.between(1, 4094), // IEEE 802.1Q valid range
|
||||
Schema.brand("VlanId")
|
||||
)
|
||||
export type VlanId = typeof VlanId.Type;
|
||||
|
||||
export const SubnetCidr = Schema.String.pipe(
|
||||
Schema.pattern(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/),
|
||||
Schema.brand("SubnetCidr")
|
||||
)
|
||||
|
||||
export type SubnetCidr = typeof SubnetCidr.Type;
|
||||
|
||||
export const NetworkSegment = Schema.Struct({
|
||||
name: Schema.String,
|
||||
vlanId: VlanId,
|
||||
subnet: SubnetCidr,
|
||||
gateway: IpAddress,
|
||||
})
|
||||
```
|
||||
|
||||
#### Use Ubiquitous Language
|
||||
|
||||
Domain types should use terminology from the problem domain:
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Technical/generic terms
|
||||
interface Config {
|
||||
val: number
|
||||
data: string
|
||||
}
|
||||
|
||||
// ✅ GOOD: Domain language
|
||||
export const TestSection = Schema.Struct({
|
||||
name: TestSectionName,
|
||||
estimatedDuration: EstimatedDuration,
|
||||
vmSpecs: Schema.Array(VmSpec),
|
||||
requiresNestedVirt: Schema.Boolean
|
||||
})
|
||||
```
|
||||
|
||||
## Tagged Error Handling
|
||||
|
||||
### ❌ DON'T: Throw generic errors
|
||||
|
||||
Never use generic `Error` or `throw` statements in Effect code.
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - Generic Error loses type information
|
||||
const validateTask = (data: unknown): Effect.Effect<Task, Error> => {
|
||||
return Effect.try({
|
||||
try: () => {
|
||||
if (!data) {
|
||||
throw new Error("Invalid data"); // ❌ Generic error
|
||||
}
|
||||
return data as Task;
|
||||
},
|
||||
catch: (e) => new Error(String(e)) // ❌ Loses error context
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### ✅ DO: Use tagged error classes
|
||||
|
||||
|
||||
```typescript
|
||||
class ValidationError extends Data.TaggedError("ValidationError")<{
|
||||
message: string
|
||||
}> {}
|
||||
|
||||
|
||||
// Use Effect.fail instead of throw
|
||||
const validateTask = (data: unknown): Effect.Effect<Task, ValidationError> => {
|
||||
if (!data || typeof data !== "object") {
|
||||
return Effect.fail(new ValidationError("Data must be an object")); // ✅
|
||||
}
|
||||
|
||||
const task = data as any;
|
||||
if (!task.text) {
|
||||
return Effect.fail(new ValidationError("Missing required field: text")); // ✅
|
||||
}
|
||||
|
||||
return Effect.succeed(task as Task);
|
||||
};
|
||||
```
|
||||
|
||||
### Why Tagged Errors?
|
||||
|
||||
1. **Type-safe error handling**: Function signatures show exactly what can fail
|
||||
2. **Pattern matching**: Use `Effect.catchTag` to handle specific errors
|
||||
3. **Better debugging**: Error `_tag` shows up in stack traces
|
||||
4. **Self-documenting**: Compiler enforces handling all error cases
|
||||
5. **Composable**: Errors compose through Effect chains
|
||||
|
||||
### Pattern Matching on Errors
|
||||
|
||||
```typescript
|
||||
const program = pipe(
|
||||
loadTask(id),
|
||||
// Catch specific error types by tag
|
||||
Effect.catchTag("NotFoundError", (error) =>
|
||||
Effect.succeed(createDefaultTask(error.id))
|
||||
),
|
||||
Effect.catchTag("ValidationError", (error) =>
|
||||
Effect.fail(new BadRequestError(error.message))
|
||||
),
|
||||
// Catch all remaining errors
|
||||
Effect.catchAll((error) =>
|
||||
Effect.fail(new UnknownError(String(error)))
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Effect.fail vs throw
|
||||
|
||||
```typescript
|
||||
// ❌ NEVER use throw
|
||||
const bad = () => {
|
||||
if (condition) {
|
||||
throw new Error("Bad!"); // ❌ Not type-safe
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ ALWAYS use Effect.fail
|
||||
const good = () => {
|
||||
if (condition) {
|
||||
return Effect.fail(new ValidationError("Good!")); // ✅ Type-safe
|
||||
}
|
||||
return Effect.succeed(result);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## pipe vs Effect.gen
|
||||
|
||||
### DEFAULT: Always use pipe
|
||||
|
||||
**Golden Rule: Use `pipe` everywhere by default. Remember to use effects `do` notation
|
||||
|
||||
### ✅ Use pipe for linear chains
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Linear transformation chain
|
||||
const updateTaskText = (id: TaskId, newText: string) =>
|
||||
pipe(
|
||||
repo.getById(id),
|
||||
Effect.flatMap((task) => updateText(task, newText)),
|
||||
Effect.tap((updated) => repo.save(updated))
|
||||
);
|
||||
```
|
||||
|
||||
### ✅ Use pipe even with conditionals
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Simple conditional with ternary
|
||||
const completeTask = (id: TaskId) =>
|
||||
pipe(
|
||||
repo.getById(id),
|
||||
Effect.flatMap((task) =>
|
||||
task.state._tag === "Done"
|
||||
? Effect.succeed(task) // Already done
|
||||
: pipe(
|
||||
transitionState(task, Done),
|
||||
Effect.tap((updated) => repo.save(updated))
|
||||
)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### ❌ Bad: Nested pipes become unreadable
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - Too many nested pipes (hard to read)
|
||||
const complexOperation = (id: TaskId) =>
|
||||
pipe(
|
||||
repo.getById(id),
|
||||
Effect.flatMap((task) =>
|
||||
pipe(
|
||||
repo.getParent(task.parentId),
|
||||
Effect.flatMap((parent) =>
|
||||
pipe(
|
||||
repo.getChildren(task.id),
|
||||
Effect.flatMap((children) =>
|
||||
pipe(
|
||||
validateHierarchy(parent, task, children),
|
||||
Effect.flatMap((valid) =>
|
||||
valid
|
||||
? pipe(
|
||||
updateTask(task),
|
||||
Effect.flatMap(() => notifyParent(parent))
|
||||
)
|
||||
: Effect.fail(new ValidationError("Invalid"))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
); // 😱 Pyramid of doom!
|
||||
```
|
||||
|
||||
## Service Pattern
|
||||
|
||||
### Effect.Service Pattern (Official)
|
||||
|
||||
Use the official `Effect.Service` class pattern from the docs.
|
||||
|
||||
// TODO convert to do notation
|
||||
|
||||
```typescript
|
||||
import { Effect, Layer } from "effect";
|
||||
|
||||
export class TaskQueryService extends Effect.Service<TaskQueryService>()(
|
||||
"TaskQueryService",
|
||||
{
|
||||
effect: Effect.gen(function* () {
|
||||
// 1. Get dependencies from Context
|
||||
const repo = yield* TaskRepository;
|
||||
|
||||
// 2. Define operations as const functions
|
||||
const getTask = (id: TaskId) => repo.getById(id);
|
||||
|
||||
const getAllTasks = () => repo.getAll();
|
||||
|
||||
const getTaskTree = (rootId: TaskId) =>
|
||||
pipe(
|
||||
repo.getAll(),
|
||||
Effect.map((tasks) => buildTaskTree(tasks, rootId)),
|
||||
Effect.flatMap((tree) =>
|
||||
tree
|
||||
? Effect.succeed(tree)
|
||||
: Effect.fail(NotFoundError.make(rootId))
|
||||
)
|
||||
);
|
||||
|
||||
// 3. Return service object as const
|
||||
return {
|
||||
getTask,
|
||||
getAllTasks,
|
||||
getTaskTree,
|
||||
} as const;
|
||||
}),
|
||||
dependencies: [], // Can list dependencies for Layer.provideMerge
|
||||
}
|
||||
) {
|
||||
// 4. Optional: Add Test layer
|
||||
static Test = this.Default.pipe(
|
||||
Layer.provide(InMemoryTaskRepositoryTest)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Use `Effect.gen` inside `effect` (this is the ONE place it's OK)
|
||||
- Close over dependencies with `yield*`
|
||||
- Return operations as `const` object
|
||||
- `.Default` provides the live layer automatically
|
||||
- `.Test` can provide mock layers
|
||||
|
||||
### Using Services
|
||||
|
||||
```typescript
|
||||
// In application code
|
||||
export const updateName = (
|
||||
task: Task,
|
||||
newName: validName
|
||||
): E.Effect<void, TaskError, PouchDBRepositoryLive> =>
|
||||
pipe(
|
||||
PouchDBRepositoryLive,
|
||||
E.tap((db) => db.create({...task, name: newName})), // the output is still the dep
|
||||
E.tap((db) => db.sync()), // the output is still the dep
|
||||
E.asVoid, // discarding all and returning void
|
||||
);
|
||||
|
||||
const program = Effect.gen(function* () {
|
||||
const queries = yield* TaskQueryService;
|
||||
const tasks = yield* queries.getAllTasks();
|
||||
return tasks;
|
||||
});
|
||||
|
||||
// With proper layers
|
||||
const AppLive = Layer.mergeAll(
|
||||
PouchDBRepositoryLive(db),
|
||||
TaskQueryService.Default,
|
||||
TaskCommandService.Default
|
||||
);
|
||||
|
||||
Effect.runPromise(program.pipe(Effect.provide(AppLive)));
|
||||
```
|
||||
|
||||
#### Railway-Oriented Programming
|
||||
|
||||
Use Effect's error handling for domain workflows:
|
||||
|
||||
```typescript
|
||||
// Each step can succeed or fail
|
||||
const provisionWorkflow = (spec: VmSpec) =>
|
||||
Effect.gen(function* () {
|
||||
// Validate
|
||||
yield* validateVmSpec(spec) // Effect<void, ValidationError>
|
||||
|
||||
// Allocate resources
|
||||
const allocation = yield* allocateResources(spec) // Effect<Allocation, ResourceError>
|
||||
|
||||
// Provision
|
||||
const vm = yield* provisionVm(spec, allocation) // Effect<VM, ProvisionError>
|
||||
|
||||
// Configure
|
||||
yield* configureVm(vm) // Effect<void, ConfigError>
|
||||
|
||||
return vm
|
||||
}).pipe(
|
||||
// Handle errors at appropriate level
|
||||
Effect.catchTag("ValidationError", (e) => /* handle */),
|
||||
Effect.catchTag("ResourceError", (e) => /* handle */),
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Service Definition Pattern
|
||||
|
||||
Always use `Effect.Service()()` pattern with explicit dependencies:
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Static methods, context.Tag
|
||||
export class SSHConnection extends Context.Tag("SSHConnection")<...>() {
|
||||
static live = Layer.effect(...)
|
||||
}
|
||||
|
||||
// ✅ GOOD: Service pattern with dependencies
|
||||
export class SSHConnection extends Effect.Service<SSHConnection>()(
|
||||
"SSHConnection",
|
||||
{
|
||||
effect: Effect.gen(function* () {
|
||||
// Implementation
|
||||
return {
|
||||
execute: (cmd: Command) => // ...
|
||||
}
|
||||
}),
|
||||
dependencies: [] // Explicit dependencies
|
||||
}
|
||||
) {
|
||||
static Test = Layer.succeed(SSHConnection, {
|
||||
execute: () => Effect.succeed(mockResult)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Schema: Use Schema.Struct, Not Schema.Class
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Schema.Class with methods using `this`
|
||||
export class Port extends Schema.Class<Port>("Port")({
|
||||
value: Schema.Number
|
||||
}) {
|
||||
toString(): string {
|
||||
return this.value.toString() // Using `this`!
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ GOOD: Schema.Struct + pure functions
|
||||
export const Port = Schema.Struct({
|
||||
value: Schema.Number.pipe(Schema.int(), Schema.between(1, 65535))
|
||||
})
|
||||
export interface Port extends Schema.Schema.Type<typeof Port> {}
|
||||
|
||||
// Pure function instead of method
|
||||
export const portToString = (port: Port): string =>
|
||||
port.value.toString()
|
||||
|
||||
// Or namespace for organization
|
||||
export namespace Port {
|
||||
export const toString = (port: Port): string =>
|
||||
port.value.toString()
|
||||
|
||||
export const SSH = { value: 22 } as Port
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Never Use `this` - Use Functional Style
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Using `this`
|
||||
class CommandResult {
|
||||
succeeded(): boolean {
|
||||
return this.exitCode.value === 0
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ GOOD: Pure function
|
||||
export const isSuccess = (result: CommandResult): boolean =>
|
||||
result.exitCode.value === 0
|
||||
|
||||
// Or namespace pattern
|
||||
export namespace CommandResult {
|
||||
export const isSuccess = (result: CommandResult): boolean =>
|
||||
result.exitCode.value === 0
|
||||
|
||||
export const isFailed = (result: CommandResult): boolean =>
|
||||
!isSuccess(result)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. One Concept Per File
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Multiple concepts in one file
|
||||
// Command.ts
|
||||
export class Command { }
|
||||
export class CommandResult { }
|
||||
export class ExitCode { }
|
||||
export class Stdout { }
|
||||
export class Stderr { }
|
||||
|
||||
// ✅ GOOD: One concept per file
|
||||
// Command.ts
|
||||
export const Command = Schema.Struct({ ... })
|
||||
|
||||
// CommandResult.ts
|
||||
export const CommandResult = Schema.Struct({ ... })
|
||||
|
||||
// ExitCode.ts
|
||||
export const ExitCode = Schema.Struct({ ... })
|
||||
|
||||
// Stdout.ts
|
||||
export const Stdout = Schema.Struct({ ... })
|
||||
|
||||
// Stderr.ts
|
||||
export const Stderr = Schema.Struct({ ... })
|
||||
```
|
||||
|
||||
### 6. No For Loops - Use Functional Iteration
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Imperative for loop
|
||||
for (const segment of topology.segments) {
|
||||
const result = await test(segment)
|
||||
results.push(result)
|
||||
}
|
||||
|
||||
// ✅ GOOD: Array methods for calculations
|
||||
const totalCpus = vmSpecs.map(spec => spec.cpus.value)
|
||||
.reduce((sum, cpu) => sum + cpu, 0)
|
||||
|
||||
// ✅ GOOD: ReadonlyArray.map for immutability
|
||||
import { ReadonlyArray } from "effect"
|
||||
|
||||
const mapped = ReadonlyArray.map(
|
||||
segments,
|
||||
(segment) => segment.name
|
||||
)
|
||||
```
|
||||
|
||||
### 7. Avoid Primitive Obsession
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Primitives everywhere
|
||||
function provision(name: string, cpus: number, memory: number)
|
||||
|
||||
// ✅ GOOD: Rich domain types
|
||||
export const VmName = Schema.String.pipe(
|
||||
Schema.pattern(/^[a-z0-9-]+$/),
|
||||
Schema.brand("VmName")
|
||||
)
|
||||
export type VmName = Schema.Schema.Type<typeof VmName>
|
||||
|
||||
export const CpuCount = Schema.Number.pipe(
|
||||
Schema.int(),
|
||||
Schema.between(1, 64),
|
||||
Schema.brand("CpuCount")
|
||||
)
|
||||
export type CpuCount = Schema.Schema.Type<typeof CpuCount>
|
||||
|
||||
function provision(spec: VmSpec)
|
||||
```
|
||||
|
||||
|
||||
### 9. Layer Composition Patterns
|
||||
|
||||
```typescript
|
||||
// Use pipe for clean composition
|
||||
export const AppLayer = Layer.merge(
|
||||
ConfigLayer,
|
||||
LoggerLayer
|
||||
).pipe(
|
||||
Layer.provide(DatabaseLayer),
|
||||
Layer.provide(ResourceLayer)
|
||||
)
|
||||
|
||||
// Factory functions for parameterized layers
|
||||
export const makeSSHLayer = (connection: HostConnection) =>
|
||||
Layer.effect(
|
||||
SSHConnection,
|
||||
Effect.gen(function* () {
|
||||
// Implementation
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Repository Pattern
|
||||
|
||||
```typescript
|
||||
export class UsersRepo extends Effect.Service<UsersRepo>()(
|
||||
"UsersRepo",
|
||||
{
|
||||
effect: Effect.gen(function* () {
|
||||
const sql = yield* Sql
|
||||
|
||||
return {
|
||||
findById: (id: string) =>
|
||||
sql.query(/* ... */).pipe(
|
||||
Effect.map(rows => rows[0])
|
||||
),
|
||||
|
||||
create: (user: User) =>
|
||||
sql.query(/* ... */)
|
||||
}
|
||||
}),
|
||||
dependencies: [SqlLayer]
|
||||
}
|
||||
) {
|
||||
static Test = Layer.succeed(UsersRepo, {
|
||||
findById: (id: string) => Effect.succeed(mockUser),
|
||||
create: (user: User) => Effect.succeed(user)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Checklist for Code Review
|
||||
|
||||
- [ ] No `this` keyword anywhere
|
||||
- [ ] No `for`/`while` loops - use functional iteration and `pipe`
|
||||
- [ ] Schema.Struct, not Schema.Class
|
||||
- [ ] One concept per file
|
||||
- [ ] Services use `Effect.Service()()` pattern
|
||||
- [ ] Branded types for domain concepts
|
||||
- [ ] No primitive obsession
|
||||
- [ ] Layer composition uses pipe
|
||||
- [ ] Tagged errors for error handling
|
||||
- [ ] Resources use Layer.scoped + Effect.acquireRelease
|
||||
|
||||
## Domain-Driven Design Workflow
|
||||
|
||||
Following Scott Wlaschin's approach:
|
||||
|
||||
### 1. Understand the Domain
|
||||
- Talk to domain experts
|
||||
- Learn ubiquitous language
|
||||
- Identify bounded contexts early enough to guide naming and ownership
|
||||
- Treat those contexts as provisional boundaries that may later split or merge under real change pressure
|
||||
|
||||
### 2. Model the Domain
|
||||
```typescript
|
||||
// Start with types that match domain language
|
||||
export const NetworkSegment = Schema.Struct({
|
||||
name: Schema.String, // "Management", "DMZ"
|
||||
vlanId: VlanId, // 10, 20, 30...
|
||||
subnet: SubnetCidr, // "10.10.0.0/16"
|
||||
gateway: IpAddress, // "10.10.0.1"
|
||||
accessRules: Schema.Array(Schema.String)
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,135 @@
|
||||
# Practicing TDFDDD by Hand
|
||||
|
||||
This tutorial is for humans who want to build judgment before reviewing LLM-generated design artifacts.
|
||||
The goal is to practice the method manually so you know what a good design artifact should feel like.
|
||||
|
||||
## What you are practicing
|
||||
|
||||
You are not trying to write production code yet.
|
||||
You are practicing how to move from a business story to:
|
||||
|
||||
- commands
|
||||
- events
|
||||
- state variants
|
||||
- a policy signature
|
||||
- a final contract
|
||||
|
||||
## How to use this tutorial
|
||||
|
||||
Pick a simple domain story and work through the phases on paper or in a scratch markdown file.
|
||||
Do not look at implementation code until the design is stable.
|
||||
|
||||
A good starter prompt is:
|
||||
|
||||
> A librarian checks out a book to a member. If the member is suspended or the book is already on loan, checkout fails.
|
||||
|
||||
## Step 1: Write the story in plain language
|
||||
|
||||
Write two or three sentences that describe what a person is trying to do.
|
||||
Do not mention framework details.
|
||||
Do not mention databases.
|
||||
Do not mention HTTP.
|
||||
|
||||
Example:
|
||||
|
||||
> A librarian attempts to check out a book to a member. The checkout succeeds only if the member is active and the book is available.
|
||||
|
||||
## Step 2: Identify the command and events
|
||||
|
||||
Write down:
|
||||
|
||||
- the command: what someone is attempting
|
||||
- the success event: what fact becomes true if it works
|
||||
- the failure event: what fact becomes true if it does not
|
||||
|
||||
Example:
|
||||
|
||||
- Command: `CheckOutBook`
|
||||
- Success event: `BookCheckedOut`
|
||||
- Failure event: `CheckoutRefused`
|
||||
|
||||
If you cannot name the events clearly, your understanding is probably still fuzzy.
|
||||
|
||||
## Step 3: Ask what information the decision actually needs
|
||||
|
||||
Now sketch the policy without worrying about perfect types.
|
||||
Use placeholder names if necessary.
|
||||
|
||||
```fsharp
|
||||
checkOut : NeededBookInfo -> NeededMemberInfo -> Result<BookCheckedOut, CheckoutRefused>
|
||||
```
|
||||
|
||||
Then replace the vague placeholders with the actual concepts the rule depends on.
|
||||
This is where the workflow starts shaping the model.
|
||||
|
||||
## Step 4: Model the minimum useful types
|
||||
|
||||
Define the nouns the policy needs.
|
||||
Prefer explicit domain concepts over primitives.
|
||||
|
||||
Ask yourself:
|
||||
|
||||
- Do I need a state distinction here?
|
||||
- Is this really one thing, or a union of stages?
|
||||
- Am I hiding business meaning inside a generic `status` field?
|
||||
|
||||
Example direction:
|
||||
|
||||
```fsharp
|
||||
type ActiveMember = { MemberId: MemberId }
|
||||
type SuspendedMember = { MemberId: MemberId; Reason: SuspensionReason }
|
||||
type AvailableBook = { BookId: BookId }
|
||||
type LoanedBook = { BookId: BookId; DueDate: DueDate }
|
||||
```
|
||||
|
||||
## Step 5: Freeze the contract
|
||||
|
||||
Once the concepts are clear, write the final signatures.
|
||||
|
||||
```fsharp
|
||||
decide : AvailableBook -> ActiveMember -> Result<BookCheckedOut, CheckoutRefused>
|
||||
apply : LibraryState -> BookCheckedOut -> LibraryState
|
||||
workflow : BookId -> MemberId -> Effect<CheckoutResponse>
|
||||
```
|
||||
|
||||
At this point the design should feel boring in a good way.
|
||||
The remaining implementation work should mostly be translation.
|
||||
|
||||
## Step 6: Review your own artifact
|
||||
|
||||
Before looking at a reference example, inspect your work.
|
||||
|
||||
Ask:
|
||||
|
||||
- Did I name the command, events, and states in domain language?
|
||||
- Did I model meaningful lifecycle distinctions explicitly?
|
||||
- Does the policy return facts instead of booleans?
|
||||
- Is the workflow contract clearly separate from the policy contract?
|
||||
- Could another person implement this without inventing missing domain meaning?
|
||||
|
||||
## Step 7: Compare against a reference
|
||||
|
||||
Now compare your work against:
|
||||
|
||||
- `worked-example-truck-loading.md`
|
||||
- `../reference/design-artifact-template.md`
|
||||
- `../reference/review-checklist.md`
|
||||
|
||||
Do not compare for identical names.
|
||||
Compare for shape, clarity, and separation of concerns.
|
||||
|
||||
## Practice loop
|
||||
|
||||
To build review skill, repeat this exercise with three kinds of stories:
|
||||
|
||||
- permission decisions
|
||||
- state transition decisions
|
||||
- allocation decisions
|
||||
|
||||
Examples:
|
||||
|
||||
- approve an expense request
|
||||
- schedule a technician visit
|
||||
- reserve inventory for an order
|
||||
|
||||
The more hand practice you do, the easier it becomes to spot weak LLM output.
|
||||
@@ -0,0 +1,238 @@
|
||||
# Worked Example: Truck Loading
|
||||
|
||||
This tutorial is a complete walkthrough of a TDFDDD design from story to final artifact.
|
||||
Use it as the primary reference example when learning the method or reviewing LLM-generated designs.
|
||||
|
||||
## The story
|
||||
|
||||
A warehouse worker attempts to put a package on a truck.
|
||||
If the package fits and the truck is open for loading, it succeeds.
|
||||
If the truck is full or sealed, it fails.
|
||||
|
||||
Later we learn an additional detail: some trucks may be unavailable because they are in transit or under maintenance.
|
||||
|
||||
## Phase 1: Event storming
|
||||
|
||||
### Command
|
||||
|
||||
`LoadPackage`
|
||||
|
||||
### Business timeline
|
||||
|
||||
1. A warehouse worker attempts to load a package onto a truck.
|
||||
2. The system checks the truck state and capacity rules.
|
||||
3. The system records either a success fact or a failure fact.
|
||||
|
||||
### Events
|
||||
|
||||
- `LoadPackage`
|
||||
- `PackageLoaded`
|
||||
- `LoadRefused`
|
||||
|
||||
### What we learned from the domain
|
||||
|
||||
The truck is not just a generic object with a `status` field.
|
||||
Some states are loadable and some are not.
|
||||
That is a clue that the model should likely distinguish the states explicitly.
|
||||
|
||||
## Phase 2: Core sketch
|
||||
|
||||
Now ask the design question:
|
||||
|
||||
> To decide if a package can be loaded, what information must the policy know?
|
||||
|
||||
Initial sketch:
|
||||
|
||||
```fsharp
|
||||
decideLoad : packageWeight ->
|
||||
packageVolume ->
|
||||
truckRemainingVolume ->
|
||||
truckRemainingWeight ->
|
||||
Result<PackageLoaded, LoadRefused>
|
||||
```
|
||||
|
||||
This sketch is intentionally rough.
|
||||
Its job is to reveal the concepts we need to model properly.
|
||||
|
||||
## Phase 3: Domain modeling
|
||||
|
||||
### Primitives
|
||||
|
||||
```fsharp
|
||||
type Weight = float<kg>
|
||||
type Volume = float<m^3>
|
||||
type PackageId = string
|
||||
type TruckId = string
|
||||
```
|
||||
|
||||
### Compound objects
|
||||
|
||||
```fsharp
|
||||
type Package = {
|
||||
Id: PackageId
|
||||
Weight: Weight
|
||||
Volume: Volume
|
||||
}
|
||||
|
||||
type TruckCapacity = {
|
||||
MaxWeight: Weight
|
||||
MaxVolume: Volume
|
||||
}
|
||||
|
||||
type CurrentLoad = {
|
||||
TotalWeight: Weight
|
||||
TotalVolume: Volume
|
||||
}
|
||||
```
|
||||
|
||||
### State variants
|
||||
|
||||
We split the truck into loadable and non-loadable states instead of hiding the difference in one broad structure.
|
||||
|
||||
```fsharp
|
||||
type LoadingTruck = {
|
||||
Id: TruckId
|
||||
Capacity: TruckCapacity
|
||||
CurrentLoad: CurrentLoad
|
||||
}
|
||||
|
||||
type SealedTruck = {
|
||||
Id: TruckId
|
||||
Status: "Sealed" | "InTransit" | "Maintenance"
|
||||
}
|
||||
|
||||
type Truck =
|
||||
| Loading of LoadingTruck
|
||||
| Sealed of SealedTruck
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
The policy returns domain facts, not UI responses.
|
||||
|
||||
```fsharp
|
||||
type LoadFailureReason =
|
||||
| OverWeight of { Limit: Weight; Actual: Weight }
|
||||
| OverVolume of { Limit: Volume; Actual: Volume }
|
||||
| TruckUnavailable of { Status: string }
|
||||
|
||||
type PackageLoaded = {
|
||||
TruckId: TruckId
|
||||
PackageId: PackageId
|
||||
NewLoadState: CurrentLoad
|
||||
Timestamp: DateTime
|
||||
}
|
||||
|
||||
type LoadRefused = {
|
||||
Reason: LoadFailureReason
|
||||
TruckId: TruckId
|
||||
}
|
||||
```
|
||||
|
||||
## Why this model is better than a generic status object
|
||||
|
||||
A single `Truck` object with a generic `status` field would force every caller to remember which combinations are valid.
|
||||
By distinguishing `LoadingTruck` from `SealedTruck`, the design moves that rule into the type system.
|
||||
|
||||
This is the kind of move a reviewer should look for in LLM output.
|
||||
|
||||
## Phase 4: The contract
|
||||
|
||||
```fsharp
|
||||
decide : Package -> LoadingTruck -> Result<PackageLoaded, LoadRefused>
|
||||
apply : LoadingTruck -> PackageLoaded -> LoadingTruck
|
||||
loadPackage : TruckId -> PackageId -> Effect<LoadResponse>
|
||||
```
|
||||
|
||||
Notice the separation:
|
||||
|
||||
- `decide` is pure and domain-focused
|
||||
- `apply` is pure and mechanical
|
||||
- `loadPackage` is the impure workflow boundary
|
||||
|
||||
## Phase 5: Implementation shape
|
||||
|
||||
Once the design is frozen, the code follows the standard orchestration pattern:
|
||||
|
||||
1. Gather data.
|
||||
2. Call the policy.
|
||||
3. Match on the returned event.
|
||||
4. Apply the state transition.
|
||||
5. Persist and return a workflow response.
|
||||
|
||||
That is why a good design artifact makes implementation feel like translation.
|
||||
|
||||
## Final artifact
|
||||
|
||||
This is the kind of finished design artifact a human reviewer should be able to compare against.
|
||||
|
||||
```fsharp
|
||||
// 1. Primitives
|
||||
type Weight = float<kg>
|
||||
type Volume = float<m^3>
|
||||
type PackageId = string
|
||||
type TruckId = string
|
||||
|
||||
// 2. The Inputs (The Objects)
|
||||
type Package = {
|
||||
Id: PackageId
|
||||
Weight: Weight
|
||||
Volume: Volume
|
||||
}
|
||||
|
||||
type TruckCapacity = {
|
||||
MaxWeight: Weight
|
||||
MaxVolume: Volume
|
||||
}
|
||||
|
||||
type CurrentLoad = {
|
||||
TotalWeight: Weight
|
||||
TotalVolume: Volume
|
||||
}
|
||||
|
||||
// 3. The States (The Aggregates)
|
||||
type LoadingTruck = {
|
||||
Id: TruckId
|
||||
Capacity: TruckCapacity
|
||||
CurrentLoad: CurrentLoad
|
||||
}
|
||||
|
||||
type SealedTruck = {
|
||||
Id: TruckId
|
||||
Status: "Sealed" | "InTransit" | "Maintenance"
|
||||
}
|
||||
|
||||
type Truck =
|
||||
| Loading of LoadingTruck
|
||||
| Sealed of SealedTruck
|
||||
|
||||
// 4. The Events (The Outputs)
|
||||
type LoadFailureReason =
|
||||
| OverWeight of { Limit: Weight; Actual: Weight }
|
||||
| OverVolume of { Limit: Volume; Actual: Volume }
|
||||
| TruckUnavailable of { Status: string }
|
||||
|
||||
type PackageLoaded = {
|
||||
TruckId: TruckId
|
||||
PackageId: PackageId
|
||||
NewLoadState: CurrentLoad
|
||||
Timestamp: DateTime
|
||||
}
|
||||
|
||||
type LoadRefused = {
|
||||
Reason: LoadFailureReason
|
||||
TruckId: TruckId
|
||||
}
|
||||
```
|
||||
|
||||
## What to pay attention to when learning from this example
|
||||
|
||||
Do not memorize the truck domain.
|
||||
Pay attention to the structure of the reasoning:
|
||||
|
||||
- the story becomes a command and events
|
||||
- the policy sketch exposes the needed information
|
||||
- the types become more specific as the design clarifies
|
||||
- the final contract separates design concerns cleanly
|
||||
|
||||
That structure is what you should reproduce by hand and review in LLM output.
|
||||
Reference in New Issue
Block a user