Initial commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user