Copilot and the assumption budget

Months with GitHub Copilot's technical preview showed that generated code saved real typing when the surrounding contract could absorb and expose its hidden assumptions.

The first code GitHub Copilot completed for me was correct, useful, and slightly dangerous for exactly that reason.

During the 2021 technical preview, I used inline completion while adding a parser for Z29C's workflow fixtures. After a function name and type signature, it proposed most of the transformation. The code compiled. It passed the straightforward example. It looked like the implementation I had been about to write.

It also converted a missing timestamp into the Unix epoch. In many programs that would be a conventional fallback. In Z29C, missing observation time meant malformed evidence and had to remain distinguishable from a real event at time zero.

The completion saved typing by spending an assumption I had not authorized.

After several months of experiments, I began evaluating generated code by its assumption budget: how much unstated semantic choice a task could safely tolerate before verification cost and risk exceeded the mechanical work saved.

Generation changed the cost profile

The technical preview could suggest whole lines and functions from comments and nearby code. Producing a plausible first draft became dramatically cheaper for some tasks.

The cost of deciding whether the draft matched Z29C did not fall in the same proportion.

For a typed data mapper, the suggestion might save ten minutes and take two minutes to verify. For retry policy, it might save five minutes of typing and introduce an hour of false confidence around one exception branch. For an unfamiliar library API, the code might arrive instantly while documentation and runtime verification remained mandatory.

I separated:

production cost
  time and attention to create a candidate implementation
 
verification cost
  time and attention to establish that it satisfies the real contract
 
assumption risk
  consequence of a plausible unstated choice escaping verification

Copilot reduced production cost. Good use required spending the difference on verification, not treating it as proof that the whole task had become cheaper.

A budget belonged to the task, not the number of lines

Some tasks had a generous assumption budget:

  • Repetitive fixture construction from a complete behavior table.
  • Mechanical adapters between explicit types.
  • Exhaustive switch scaffolding whose cases were already defined.
  • Serialization round-trip tests with a clear invariant.
  • Local pure transformations whose output was easy to inspect.

Others had almost none:

  • Authorization and privacy boundaries.
  • Idempotency and retry semantics.
  • Workflow-definition and schema migration.
  • Outcome-unknown recovery.
  • Error handling where a default changed evidentiary meaning.
  • External APIs whose version or operational contract was uncertain.

The distinction was consequence and verifiability, not complexity. A forty-line fixture generator could be safe. A one-line ?? 0 fallback could corrupt the incident timeline.

I asked: if this suggestion chooses a common default I did not notice, how far can that decision travel?

The answer set the review depth before generation began.

Plausibility removed useful pauses

When I wrote code manually, a missing case often produced a pause. I had to choose a state, look up an API, or ask whether a value could be absent.

Completion could remove the pause by selecting the most plausible continuation. The uninterrupted flow felt productive. The pause had been information: the implementation had reached a decision boundary.

I learned to recreate that boundary before accepting a substantial suggestion. Types, behavior tables, assertions, and comments stated the contract first.

For the fixture timestamp:

valid timestamp   → parsed instant
missing timestamp → validation issue
invalid timestamp → issue preserving source location
unknown field     → retain for forward compatibility

Completion inside that frame remained fast. Deviations became visible.

The goal was not to slow every suggestion down. It was to keep fluent code from erasing the moments where the product needed a decision.

Copilot produced useful test scaffolding. It could also infer expected behavior from the implementation it had just helped create.

If the parser normalized missing time to epoch zero, a generated test could assert epoch zero. The code and test would agree and the domain would lose.

I separated test cases by source:

  • Domain cases came from the workflow state model, incidents, failure analysis, and manually reviewed behavior tables.
  • Completion expanded permutations, setup, and repetitive assertions.
  • Property tests came from invariants independent of implementation shape.

Z29C properties included:

  • One idempotency key never maps to two command digests.
  • A succeeded step always has a receipt.
  • Lease expiry closes the old worker’s path to current step state.
  • Outcome unknown never advances a dependant requiring an external reference.
  • Replaying a projection produces the same current state.

Generated tests became powerful after I supplied the semantic oracle. Before that, they risked making a plausible implementation self-validating.

The preview drew context from the code being edited and related editor state. Z29C's real contract lived across step definitions, adapter fixtures, policy decisions, Work notes, and incidents from earlier projects.

No single file said that missing timestamps must fail because source-time integrity underpinned W93H and Z29C evidence. A local suggestion could not be blamed for absent architecture.

I improved the codebase for human and generated readers:

  • Domain types represented impossible states less easily.
  • Stable step IDs differed from display names.
  • Error codes were exhaustive rather than free strings.
  • Small helpers owned one non-obvious policy.
  • Scenario names described the failure boundary.
  • Comments explained constraints that types could not.

I did not scatter prompt-like prose through files solely to control completions. The added context had to help a maintainer without Copilot.

Better architecture reduced the amount of implicit repository knowledge any candidate implementation needed.

The boring code was not always low risk

Generated tools were often described as ideal for boilerplate. Some repetition exists precisely because a boundary is important.

Mapping an authenticated subject into adapter capability, validating an external payload, redacting error fields, and migrating stored state can all look repetitive. Their semantics remain consequential.

The useful category was constrained repetition. A mapping with a closed input union and exhaustive output contract was a good candidate. An authorization middleware that looked like every other middleware was not safe merely because the pattern was familiar.

I reviewed generated boundary code for omitted cases, default branches, error behavior, and values copied too broadly. A suggestion that generalized several cases could reduce lines and erase a meaningful distinction.

Boilerplate is a visual judgment. Assumption budget is a semantic one.

The tool worked best when repetition carried little freedom.

I had adopted TypeScript gradually to make JavaScript changes safer. Generated completion made strong types valuable in a new way.

A discriminated union gave the suggestion fewer plausible but invalid states. An exhaustive switch made a missing outcome visible. Branded workflow, step, and attempt IDs reduced accidental interchange. Read-only input types discouraged mutation under a stable receipt.

Types could not encode every requirement. They would not prove an external adapter honored idempotency or that a retry was useful before a deadline. They could stop a generated helper from treating outcome-unknown as an ordinary failure if the function's signature demanded a separate result.

I preferred making illegal states hard to express over adding a long comment asking future completions not to create them.

The remaining assumption budget became smaller and easier to inspect.

Generated code did not reduce the value of types. It increased the value of machine-checkable boundaries around plausible candidates.

Suggestions could be stale about APIs

A completion sometimes proposed a method that was plausible, documented in another version, or did not exist at all. Type checking caught missing names when types were current. It did not prove operational semantics or configuration.

For any unfamiliar library or service API, I treated the suggestion as a search lead. I checked the installed version and authoritative documentation, then wrote or accepted code against that evidence.

I did not add a dependency because Copilot imported it conveniently. Dependency choice still involved maintenance, license, bundle or runtime cost, and supply-chain review.

Runtime tests covered details types could not: response shapes, status codes, timeouts, idempotency windows, and error fields. Synthetic adapters modeled operational behavior rather than only SDK signatures.

Confidence in the completion's syntax was not evidence of currency.

The verification cost remained high whenever the repository itself did not contain the answer.

Copilot could suggest common parsing, traversal, and retry patterns. I did not assume the output carried a trustworthy source explanation or licensing context.

For distinctive or unfamiliar passages, I refused code I could not explain independently. I checked authoritative sources for the algorithm or API and wrote tests from the project's constraints. A suggestion could introduce terminology or an approach worth researching; it could not serve as the reference.

I also looked for suspiciously elaborate code that exceeded the task. Generated generality can import assumptions about scale, input, and error recovery. Deleting it was often the best review step.

The final repository needed to be maintainable after the suggestion disappeared. Names, comments, tests, and docs had to carry the reason.

The rule was simple: accept only code I was prepared to own without appealing to where it came from.

Security work had a nearly zero budget

One completion for a policy cache used subject and action as its key and omitted policy version. The code defaulted safely on missing data and could preserve an old allow decision after policy changed.

It looked cautious. It was temporally unsafe.

For authority, secrets, privacy, and side effects, I wrote a short threat and state table first:

  • Allowed at workflow creation, denied at step execution.
  • Credential expires after request identity is stored.
  • Error response contains a secret header or private payload excerpt.
  • One account resumes local work created by another.
  • Recovery command arrives from a stale workflow revision.

Completion could implement an exhaustive mapping after these cases existed. It did not choose caching or fallback policy.

I also inspected suggestions for logging. A helpful debug line could serialize workflow input, credentials, or external response data into durable logs.

Security code is often short. Its assumption radius is not.

The tool sometimes offered a better alternative

Not every unexpected suggestion was a mistake. Copilot occasionally proposed a smaller standard-library operation or a test permutation I had not considered.

I evaluated alternatives through the same contract. If the implementation was clearer, supported by current APIs, and passed independent cases, I kept it. Surprise was a reason to inspect, not reject automatically.

One completion replaced a manual stable-sort decoration with an index-preserving comparison that was easier to read under the project's supported runtime. Another added a duplicate-event fixture when it recognized the existing stable ID pattern.

The value was not only saved typing. The tool widened the candidate set cheaply.

That benefit required a selection process. More alternatives can improve design when the criteria are explicit and create noise when plausibility itself becomes the criterion.

Copilot was useful as a prolific proposal generator inside a maintained decision boundary.

It was possible to spend longer auditing a completion than writing the code. Rejecting the tool entirely would avoid that trap and its benefits.

I used three rules:

  1. If the contract was local and verification cheap, accept and review the suggestion.
  2. If verification required an unfamiliar dependency, external service, or security claim, research first and treat the suggestion as a lead.
  3. If the task encoded a consequential product decision, make the decision before asking for implementation.

If a completion produced more surface than the task needed, I deleted it. If I could not state why a branch was correct, passing tests were insufficient. If verifying line by line exceeded a simple rewrite, I rewrote.

The acceptance gesture did not create sunk-cost ownership. Generated code remained disposable until the evidence justified it.

The stopping rule kept the tool an accelerator rather than an audit generator.

I kept a private log of representative tasks:

  • Context available to the tool.
  • Suggestion size and type.
  • Accepted, edited, or rejected.
  • Hidden assumption discovered.
  • Verification method.
  • Whether the session saved effort after review.

The sample was too small and varied for a meaningful universal speed number. I did not invent one.

The qualitative pattern was stable. Fixture expansion and constrained mapping often saved effort. Repository-specific state machines required careful correction. API suggestions demanded documentation. Tests were strongest when expected outcomes came from a separate source.

I also recorded when completion interrupted thought. Rapid suggestions could pull the implementation toward the first plausible structure before I had decomposed the problem.

Tool evaluation included attention, not only output.

Migrations exposed the narrowest budget

I tested completion on a Z29C workflow migration and found a useful boundary. The source definition had a step named send; the target used a stable deliver-document-v1 identity. Some workflows were pending, some held completed receipts, and one had an external request with unknown outcome.

The generated mapping handled the structural rename cleanly. It transformed rows, updated labels, and covered the common pending state. It also mapped the unknown-outcome step to the new stable ID, which would change the external idempotency key used for recovery.

The code was locally consistent and globally dangerous.

I split the task. Completion could generate the mechanical projection changes and fixture builders. I wrote the eligibility table manually:

pending before request identity → eligible for explicit migration
external request persisted      → keep original stable identity
completed receipt               → immutable historical mapping
compensation active             → keep compensation identity and graph

The migration handler then exhausted that table. Captured running-state fixtures verified every case across deployment.

This showed why “boring migration code” was not one category. Repetitive field movement had a generous budget. Deciding whether durable identity could change had none.

The suggestion remained helpful after I reduced its authority. It saved work inside the rows whose semantics had already been decided and stayed away from the decision that made the migration safe.

Copilot sometimes offered an implementation different from the one I had planned. I noticed a subtle bias: I scrutinized the unfamiliar suggestion more deeply than my familiar hand-written approach, then credited the tool only when its version survived the harder review.

I compared alternatives against the same cases. For a retry classifier, both the generated switch and my planned table-driven implementation had to handle response loss, authoritative absence, rate limit, policy denial, and stale adapter contracts. For a parser, both faced the same malformed fixtures and round-trip properties.

Sometimes the generated alternative was smaller. Sometimes my familiar approach contained assumptions I had stopped seeing. The tool's presence improved review by making a second candidate cheap, even when neither candidate was accepted directly.

I recorded the decision rather than the source of the winning draft: chosen approach, rejected alternative, evidence, and review trigger. This prevented “AI-generated” or “human-written” from becoming proxies for quality.

The same standard also protected against reverse novelty. I did not keep a surprising implementation merely because the model had found it. The simplest solution that satisfied the contract and remained maintainable won.

Generated alternatives were valuable when they widened reasoning. They were distracting when they widened only the diff.

Copilot did save time. It reduced mechanical typing, offered alternatives, and made some test expansions almost pleasant.

The saved effort could go to:

  • Clearer contracts and types.
  • More adversarial scenarios.
  • Documentation research.
  • Smaller interfaces.
  • Accessibility and recovery review.
  • Removing unnecessary code.

Or it could disappear into lower scrutiny because the code looked complete.

I made the destination explicit during review. For a generated adapter mapping, I spent the saved time on response-loss fixtures. For a parser, I added source-location errors. For repetitive UI states, I tested keyboard and screen-reader transitions.

Generation created slack only if I chose not to fill it with more generated surface.

The tool's value depended on how the project spent the difference.

Review began from the contract, not the empty file

Generated code can look impressive compared with having no implementation. That is the wrong baseline.

I compared it with the behavior table, type contract, and smallest solution. I asked:

  • Which branch expresses each required case?
  • Which branch has no required case?
  • What defaults were introduced?
  • What input or state can bypass validation?
  • Which external or version claim needs documentation?
  • Which tests originated independently?
  • What should be deleted?

This made review less about whether I liked the code and more about correspondence.

The final diff entered the same lint, type, unit, scenario, and build checks as handwritten code. AI assistance was not a category of reduced quality or special exemption.

The first draft source changed. Repository ownership did not.

A task's budget could improve through design.

Splitting a large recovery handler into pure classification and side-effect execution made the classification cheap to verify. Replacing boolean flags with a state union exposed invalid combinations. Adding an adapter contract fixture turned an external promise into executable evidence. Pinning library versions made API suggestions easier to check.

The better the surrounding constraints, the more safely Copilot could contribute.

This produced a productive feedback loop. I did not loosen standards because generation was fast. I made architecture more explicit so candidate implementations had less room to be subtly wrong.

Some work retained a low budget forever. Human judgment about policy, privacy, and product consequence did not become mechanical after a type refactor.

The budget described the gap between available constraints and required decision, not a permanent label on a file.

Plausibility was the feature and the risk

The technical preview was impressive because the suggestions often looked like code a competent developer might write. That made them useful and made subtle mistakes easy to accept.

The epoch fallback was not nonsense. It was a reasonable pattern under a different contract. Z29C needed the evidence model to reject it.

Months of use left me neither dismissive nor converted to automatic acceptance. Copilot was a valuable completion tool when the task had a clear local contract and verification path. It became expensive when the task's meaning lived outside the visible context or when a wrong conventional choice carried consequence.

Generated code arrives with the aesthetic of completion. Engineering begins by restoring the questions hidden inside it.

The assumption budget was my way of deciding how many of those questions the surrounding system could answer before I pressed Tab.

When the budget was exhausted, the right response was not a longer prompt or more hopeful acceptance. It was to make the missing decision, fetch the authoritative context, or reduce the task until its correctness became locally visible.

Completion was fastest after the thinking became explicit.