Initial commit

This commit is contained in:
ada
2026-05-25 05:47:28 +00:00
commit 4d6495ffda
97 changed files with 13403 additions and 0 deletions
+167
View File
@@ -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>` |
+87
View File
@@ -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.