Case Study, Part 4 of 5: Construction Phase
Where we are: Inception ended with a plan — one unit (synth-data-cli), one Bolt, walking skeleton = schema parsing + row generation + CSV export first. Construction now builds it, for real, in reviewable slices.
In plain English: Three quick terms this page uses a lot: a Unit is one buildable piece of the product (here, everything is small enough to be just one). A Bolt is one complete build-and-test pass over a unit. A walking skeleton is the smallest possible version that actually runs end to end — not the whole product, just enough of a "skeleton" to prove the core idea works, before fleshing out the rest.
Every prompt we actually type is highlighted like this.
Starting the first Bolt now: one build pass over the code, tests and
checks for synth-data-cli. First step is Functional Design.
Stage 3.1: Functional Design (architect agent)
Claude Code:
Schema.from_yaml(path)parses and validates the schema, raising a clear error on an unknown type or malformed constraint.generate(schema, n)yieldsnFaker-backed rows matching each column's type and constraint. One exporter function per format, each taking the same row list. Approve
Stage 3.2: NFR Requirements (devsecops agent)
NFR = Non-Functional Requirement — a rule about how well the software behaves (how fast, how much memory, how safely it fails), as opposed to a functional requirement, which is about what it does. Rather than "no significant NFRs," this project actually has two worth naming:
Claude Code: Two non-functional requirements: (1) generating 100,000 rows must not exceed 500MB of memory — ruling out building the entire result as one giant in-memory structure before writing it out; (2) an invalid schema must fail before any file is written, not partway through. Written to
nfr-requirements.md. Approve
Stage 3.3: NFR Design (architect agent)
Claude Code: Answering both: rows generate as a Python generator, consumed by each exporter in batches of 1,000 rather than materialized as one list — satisfies the memory bound. Schema validation runs to completion before
generate()is ever called — satisfies fail-before-write. Written tonfr-design.md. Approve
Stage 3.4: Infrastructure Design (aws-platform agent)
Claude Code: No servers — this ships as a pip-installable package. Build as a wheel + sdist, publish to PyPI under the name
synth-data. Written toinfrastructure-design.md. Approve
Stage 3.5: Code Generation (subagent) — Plan, Then Code
This is the walking skeleton gate — the first Construction stage is always reviewed before the rest of Construction runs, even in autonomous mode. Claude Code writes the plan first, before touching any code:
# code-generation-plan
- [ ] src/schema.py — Schema, Column, Constraint (dataclasses), from_yaml()
- [ ] src/generate.py — generate(schema, n) — generator, Faker-backed
- [ ] src/exporters.py — to_csv, to_json, to_parquet, to_sql — batch-consuming
- [ ] src/cli.py — click CLI wiring it together
- [ ] tests/test_generate.py — constraint compliance, row count, format round-trips
▸ Approve Plan? [Yes / Request Changes]
You: Approve Plan
Only now does Claude Code generate code — the plan-approval-guard hook (below) physically blocks it from writing source files before this point. A slice of src/exporters.py, showing the batch-consuming design from NFR Design above:
def to_csv(rows: Iterator[dict], path: str, batch_size: int = 1000) -> None:
with open(path, "w", newline="") as f:
writer = None
for batch in chunked(rows, batch_size):
if writer is None:
writer = csv.DictWriter(f, fieldnames=batch[0].keys())
writer.writeheader()
writer.writerows(batch)
Code generation complete — 4 files created, 9 tests written, all passing.
▸ [Approve / Request Changes]
You: Approve
The walking skeleton just shipped. Because our Delivery Plan has only one Unit, the ladder prompt — a follow-up question the AI would normally ask here, letting us choose "Continue autonomously" vs. "Gate every Bolt" (i.e., keep approving each step, or let it run ahead on its own) — has nothing further to apply to. With more than one Unit, this is the moment we'd make that choice for the remaining Bolts.
Behind the scenes:
aidlc-plan-approval-guardis a PreToolUse hook — a script that runs automatically before a tool is allowed to act, and can block it. Here, it deterministically blocks any code-writing tool call for this stage until it sees a recorded "Approve Plan" decision, so the plan-before-code ordering can't be talked around.aidlc-deliver-stage-rulesruns alongside it, making sure the exact same stage rules reach a delegated subagent as reached the conductor (the main orchestrating agent). Had our Delivery Plan split this into several independent Units, the ones with no dependency on each other would run as a parallel batch — multipleTaskcalls issued in a single turn, i.e. worked on at the same time instead of one after another.
Stage 3.6: Build and Test (once, for the whole solution)
Claude Code runs these commands itself — it has shell access as one of its tools, so nobody types pytest by hand — and reports what came back:
pip install -e . → entry point `synth-data` registered
pytest tests/ -v → 9 passed in 0.31s
You: Approve
Stage 3.7: CI Pipeline (once, for the whole solution)
Claude Code: Writes
.github/workflows/test.yml— runspytestandruff checkon every push, across a Python 3.10 / 3.11 / 3.12 matrix, so a change that works on our machine is checked on the versions we claim to support before it ever reaches Operation.
▸ Approve? [Yes / Request Changes]
You: Approve
Behind the scenes:
Progress: 23/33 overall | 7/7 CONSTRUCTION stages complete.synth-datanow exists, tested, in the project'ssrc/, with a CI workflow that will run on every future push — including whatever Operation is about to do.
‹ Back to: Part 3 — Inception Phase Continue to: Part 5 — Operation Phase