Code Completion with Doctest-Driven Validation — NO RECOVERY LOOP (Ablation A)
What is ablated: the Recovery Loop pattern. In the full skill, Step 9 writes the candidate to a temp file, runs the doctest runner, diagnoses failures, applies a targeted fix, and repeats up to
MAX_RECOVERY_ITERATIONStimes. In this variant Step 9 runs the doctest runner exactly once and records the result. There is no diagnosis, no fix, and no re-run. Whatever the first candidate produced is what gets delivered.What is preserved: Orchestration (the staged recipe, task routing, the 5-tier classification, the multi-file reference structure) and the Quality Gates (Steps 5 and 10). This isolates the marginal contribution of the Recovery Loop: comparing this variant against the full skill measures exactly what the iterative repair buys.
This skill completes missing Python code — either a full function body or the remainder of a truncated line — using a test-first approach: generate doctests that define the contract, generate code that fulfils it, then validate once.
The order still matters. Doctests before code forces you to commit to what the function should do before deciding how it does it. The difference from the full skill is that a misunderstanding surfaced by the single validation pass is reported, not repaired — there is no loop to fix it.
Folder structure
code-completion-NO-RECOVERY-LOOP/
├── SKILL.md ← you are here
├── run_doctest.py ← executes doctests against candidate code (ONE pass)
├── doctest_generator.md ← formatting rules, tier strategy, generation patterns
├── single_validation.md ← the single-pass validation step (replaces recovery_loop.md)
└── injection.md ← final assembly and delivery format
Load each reference file at the step that calls for it.
Understanding your inputs
You will receive a JSON record in one of two shapes. The single metadata key that differs between them is how you tell the tasks apart — check it explicitly, never infer from the prompt shape.
Function body completion — the prompt ends right after a function's docstring
closing """ (or after the signature : if there is no docstring). The body is
entirely absent. Generate the complete indented body.
Line completion — the prompt ends mid-line. Generate only the characters that complete that one line. Nothing more — no newline, no next line.
Routing rule: "function_name" in metadata → FUNCTION_BODY task.
"line_no" in metadata → LINE task. These are mutually exclusive.
Configuration
| Parameter | Default | What it controls |
|---|---|---|
DOCTEST_SCOPE |
"targeted" |
"targeted" = target function only; "file" = also generate for functions the target calls internally |
INJECT_DOCTESTS |
true |
Whether passing doctests stay in the final output permanently |
Note:
MAX_RECOVERY_ITERATIONSandBEST_ATTEMPT_ON_FAILUREfrom the full skill are intentionally absent — there is no loop and no across-iteration best-attempt tracking in this variant. The single validation pass is always one attempt.
The workflow
Steps run in strict order. Each produces something the next depends on.
Step 1 → Analyse the file
Step 2 → Route to task type
Step 3 → Load additional context (only what is needed)
Step 4 → Classify the target function
Step 5 → Pre-generation quality gate
Step 6 → Generate doctests [→ doctest_generator.md]
Step 7 → Validate doctest structure
Step 8 → Generate first candidate code
Step 9 → Single validation pass [→ single_validation.md] (NO recovery loop)
Step 10 → Post-generation quality gate
Step 11 → Inject and deliver [→ injection.md]
Step 1 — Analyse the file
Read the entire prompt field before doing anything else. Build a mental model of
the codebase so that whatever you generate fits naturally into it.
As you read, note:
- Every import statement and what it provides
- Module-level variables and constants
- Class names, their methods, and inheritance relationships
- Other function names visible in the prompt and what they do
- The naming style in use (snake_case, camelCase — infer from existing code)
- How existing functions handle errors: raise, return None, return a sentinel value?
- What types existing functions return
- Any decorators on the target function
- The exact docstring of the target function, verbatim, if present
Stop here if the prompt is empty or unparseable. Report the error rather than proceeding with no context.
Step 2 — Route to task type
Check the metadata keys:
"function_name"present → FUNCTION_BODY task. Record the function name andmetadata["lineno"]as the line where the body starts."line_no"present → LINE task. Recordmetadata["line_no"]as the line being completed.- Neither present → stop. Report that the task type cannot be determined.
For FUNCTION_BODY: generate everything from the first indented line of the body through the final return or expression. Match the file's indentation (4 spaces standard). Do not alter the signature or the docstring.
For LINE: generate only the characters that come after the last character of the prompt on that same line. No leading newline. No additional lines.
Step 3 — Load additional context
The prompt is your primary source. Before loading anything else, exhaust what it already tells you. Only go further if the target function references names that do not appear anywhere in the prompt and cannot be inferred from the imports.
Never load the repository's test files. Those are the external ground truth that runs after delivery. Loading them contaminates the completion.
Step 4 — Classify the target function
Before generating any doctests, classify the target function into the tier that best describes what it does. The tier determines what kind of doctests are possible.
| Tier | Type | Signal | Doctest strategy |
|---|---|---|---|
| 1 | Pure / Deterministic | No external calls, same output every time | Full input → output examples |
| 2 | Contract-Testable | Non-deterministic output but testable type/shape/range | Test isinstance, len, bounds |
| 3 | Setup-Assisted | Needs temp object, class instantiation, or asyncio.run | Set up context inline in doctest |
| 4 | Error-Path Only | Calls DB, API, network, filesystem | Test only input validation and error raises |
| 5 | Untestable | Even error paths need live external state | Document with TODO, skip doctest |
Special case for LINE tasks: First determine what syntactic context the truncated line sits in:
- Inside a function body → classify that function using the tier table above
- Inside an import block (
from x import () → Tier 5, no doctest possible - Module-level assignment or constant → Tier 5, no doctest possible
- Class attribute declaration → Tier 5, no doctest possible
For Tier 5 contexts in LINE tasks, skip doctest generation entirely and proceed directly to Step 8. Record that validation was skipped and why.
Read doctest_generator.md for detailed guidance on each tier's generation
patterns before proceeding to Step 6.
Step 5 — Pre-generation quality gate
Before writing any doctest or any code, confirm you can answer these questions. If you cannot answer one, make the assumption explicit and record your confidence level (high / medium / low).
- What does this function do? (source: docstring, function name, call sites)
- What input types does it accept?
- What does it return, and in what type?
- What exceptions should it raise, and under what conditions?
- Are all imports this implementation will need already present in the prompt?
- For LINE tasks: what syntactic construct is the truncated line completing?
Note: in the full skill, a wrong assumption here could still be caught and repaired by the recovery loop. In this variant there is no such safety net — a wrong assumption here will be carried straight through to delivery. The quality gate is therefore the only pre-delivery check on intent. Treat it accordingly.
Step 6 — Generate doctests
Read doctest_generator.md now.
Generate doctests appropriate for the tier you identified in Step 4. The goal is to define the function's contract before implementing it — what goes in, what comes out, what gets raised.
For Tier 1–2 FUNCTION_BODY tasks: generate 2–6 doctests covering the typical happy path, at least one edge case, and any documented exception. For Tier 3 FUNCTION_BODY tasks: generate setup inline in the doctest block. For Tier 4 FUNCTION_BODY tasks: generate doctests only for input validation and error-raise paths. For LINE tasks inside a function (Tier 1–3): generate a doctest for the containing function that exercises the line being completed.
All expected output values must be concrete and deterministic.
Step 7 — Validate doctest structure
Before running anything, verify the doctests are syntactically correct. Check every doctest block for:
>>>prefix with exactly one space after the arrows...prefix on continuation lines with exactly one space- Expected output on the very next line after
>>>— no blank line between them <BLANKLINE>used wherever expected output contains a blank line- Exception format exactly:
Traceback (most recent call last):then...thenExceptionType: message - No
...wildcard in expected output unless# doctest: +ELLIPSISis on the same>>>line
Fix any formatting problems before proceeding.
Step 8 — Generate first candidate code
Generate the completion using everything gathered in Steps 1–7.
For FUNCTION_BODY tasks: generate the full body, properly indented; stay consistent with existing imports; honour observed naming and error-handling patterns; keep consistent with the expected outputs committed to in Step 6; do not alter the signature or docstring.
For LINE tasks: generate only the remainder of the truncated line, starting at the exact character position where the prompt ends; no leading whitespace or newline; end at the natural conclusion of the syntactic construct; do not generate the next line.
Step 9 — Single validation pass (NO RECOVERY LOOP)
Read single_validation.md now.
Write the candidate code to a temporary file alongside the generated doctests, then run the doctest runner once:
python run_doctest.py /tmp/completion_candidate.py
Read the structured output and record it: STATUS, how many doctests passed, and any failure details. That is the entire step.
This variant does NOT:
- diagnose the failure class,
- make a targeted fix,
- re-run the runner,
- iterate up to a maximum number of attempts,
- track a best attempt across iterations.
The first candidate from Step 8 is the delivered code regardless of whether the single validation pass passed or failed. The runner output is captured as a reported signal only — it does not change the completion.
For Tier 5 / LINE instances there is no runnable oracle, so the runner is skipped
and only a syntax check (ast.parse / python -c) is performed — same as the full
skill.
Step 10 — Post-generation quality gate
Before delivering anything, verify:
- Does the completion cover only what was missing — no changes to the existing prompt?
- Is indentation consistent throughout (no mixed tabs/spaces, correct depth)?
- For LINE tasks: does the completion attach cleanly to the last character of the prompt with no spurious leading space or newline?
- Are there syntax errors in the completion?
- Does the completion reference any names not in scope? Flag but do not block.
Block on syntax errors. Flag but proceed on scope warnings.
Step 11 — Inject and deliver
Read injection.md now.
Assemble the final output:
- The original prompt, unchanged
- The completion appended at the correct position
- If
INJECT_DOCTESTS = true: doctests that PASSED the single validation pass embedded in the docstring. Doctests that FAILED the single pass are replaced with a TODO comment (a known-failing doctest is never injected).
Deliver in this structure:
task_id: <from metadata>
task_type: FUNCTION_BODY or LINE
status: VALIDATED_PASS (single pass: all doctests passed)
VALIDATED_FAIL (single pass: one or more doctests failed — delivered anyway)
SKIPPED_NO_ORACLE (Tier 5 / LINE — syntax check only)
doctests_passing: <n passing> / <n total generated>
completion: <the generated code only — not the full file>
full_output: <complete prompt + completion with passing doctests embedded>
Then tell the user, in plain language: what tier the function was, how many doctests were generated, the result of the single validation pass, and — if it failed — what the failure was (reported, not fixed, since this variant has no recovery loop).
A worked example
Input (function body task): factorial(n), Tier 1.
Step 6 doctests: factorial(0) -> 1, factorial(5) -> 120,
factorial(-1) -> ValueError.
Step 8 candidate:
if n < 0:
raise ValueError("n must be non-negative")
if n == 0:
return 1
return n * factorial(n - 1)
Step 9 (single pass): Runner reports 3/3 passed. Recorded. (Had it reported a failure, the same candidate would still be delivered — there is no loop to repair it.)
Step 11 output:
task_id: example/1
task_type: FUNCTION_BODY
status: VALIDATED_PASS
doctests_passing: 3 / 3
What this skill does not do
- Run the developer-written ground truth tests
- Modify the function signature or existing docstring prose
- Iterate, diagnose, or repair after the single validation pass (ablated)
- Load repository test files