ImplementHandler
Write the body of a class that `make` already scaffolded — through a gate, not around it. ── WHY THE UNIT IS A CLASS AND NOT A FILE PATH ────────────────────────────────────────────────── A generic file write is the ungovernable extreme: its target is a path nobody's request names, the intent contract has nothing to compare, and its blast radius is the filesystem. A CLASS in a plugin this app scaffolded is the governable unit: the target is a name the human's request can carry (the intent contract applies, ADR-0044), and the path is DERIVED — searching only inside `src/Plugins/<plugin>/` — so escaping the tree is impossible by construction, not by discipline. ── LANDING IS A POSTCONDITION, NOT A HOPE ─────────────────────────────────────────────────────── Measured on three real sessions (long-session-three-arms.tsv): everything the catalogue cannot express, the agent fakes with structure that LOOKS like the task. Raising the ceiling without a gate would trade unfilled shells for broken files. So the content verifies BEFORE it lands — syntax (`php -l` on a temp copy), strict types, the class it claims, the namespace its location dictates — and on any failure the original survives byte for byte and the diagnostic travels back, which is what a model corrects from. ── STATIC CONFORMANCE, MEASURED BEFORE IT WAS ADDED ───────────────────────────────────────────── The first real run (implement-first-landing.tsv) landed clean four times and still fataled three: invented collaborators and a narrowed interface signature — linkage defects `php -l` cannot see. Replayed against PHPStan level 0 with the app's autoloader, BOTH classes of failure were named surgically in ~1s and the good landing passed in 0.8s. So when the app ships an analyzer, the gate uses it: the candidate is analyzed IN PLACE — context needs the app's autoloader — and on any finding the original is restored byte for byte. Level 0 on purpose: this gate rules on linkage, not style; the app's own level belongs to its suite. ── THE BEHAVIORAL JUDGE: THE CLASS'S OWN TEST, INSIDE THE GATE ────────────────────────────────── Measured on a real session: a service landed conformant — syntax, interface, namespace clean — while its solicitar() SIMULATED persistence in a comment. Linkage had a judge and the app suite had one; behavior had none. The judge is never an LLM reading code (the substitute certificate P-0001 took seven generations to kill) and never the agent grading itself (the prior that faked the behavior would grade its fake): it is the test that already declares what the class must DO — `tests/Plugins/<plugin>/<Class>Test.php` — executed as one more landing postcondition, red restoring byte for byte. Without a test the landing still lands — Q-P19-R measured that OBLIGATING a criterion made everything worse — but the result SAYS the behavior went unjudged: a silent gap reads as covered, and that is how a simulated persistence ships wearing green. Running the class's test EXECUTES the written code, in a subprocess — the same risk class as the `test` operation, accepted for the same reason and said here. Fuller isolation is ADR-0045's. ── THE CAP: THE OLD CONTRACT WAS BREAKING ITS OWN CALLER ──────────────────────────────────────── Measured twice on the greenhouse fixture series (runs 10-11): a model writing a WHOLE PHP file inline as one JSON string argument broke its own tool-call JSON with high per-attempt probability at size — «Failed to parse tool call arguments as JSON … missing closing quote» at column 5,389 and at column 15,248 — and a gateway-side retry did not absorb the double flake. The root is the contract: it INVITED unbounded inline bodies. So the fix is architecture, not a nudge: content over MAX_INLINE_CHARS is refused before any write, and the refusal teaches the piece-wise door — `mode=start` writes the file header and first section, `mode=append` each next section (verbatim: the caller owns the bytes), `mode=finish` verifies and judges the assembled file through the SAME gate a single-shot passes. A partial file is not valid PHP, so start and append verify NOTHING and their results say so plainly — no green is claimable until finish.
ImplementHandler::__construct()
public function __construct(Milpa\DevTools\Support\RootResolver $roots = new RootResolver(), ?string $analyzer = null, ?string $behaviorRunner = null):Parameters
| Name | Type | Description |
|---|---|---|
| $roots | Milpa\DevTools\Support\RootResolver | |
| $analyzer | (string | null) | the static-analysis command, or `null` to derive it from the app (`vendor/bin/phpstan`, when present). Tests inject a seam here — a gate only verifiable against a running binary is a gate nobody verifies in practice |
| $behaviorRunner | (string | null) | the command that runs one test file, or `null` to derive it (`vendor/bin/phpunit`, when present) — same seam, same reason |
ImplementHandler::handle()
public function handle(array $input): arrayLand the complete body of one scaffolded class — or refuse with the diagnostic, original intact.
Parameters
| Name | Type | Description |
|---|---|---|
| $input | array<string, mixed> |