Comments Aren't Compilers

▶ Listen to this article

For about two days, a feature was completely silent. No errors in the logs. The service was up, handling requests, healthchecks passing. The feature just… wasn’t there. Nothing in the output that said it was missing. Nothing complained. It had, as far as the system was concerned, simply never happened to load.

Tracing it back: a configuration file held an allowlist of modules to load at startup. The module in question had been removed from that list during a refactor that extracted it into a separate artifact — a derived image meant to extend the base. Someone, reasonably enough, left a comment:

# pete_device: overlay provides this

The overlay’s Dockerfile copied the module’s code files into the right location. It just never patched the allowlist.

So the module’s code was present on disk. The module’s name was absent from the list of modules to load. Startup skipped it without complaint, because skipping unlisted modules is the correct behavior. Two days later, someone noticed the feature wasn’t doing anything.

What compilers are for

When you write a function and call it in a typed language, the compiler checks that the function exists, that the arguments match, that the return type is what you expect. You cannot call a function that doesn’t exist; the build fails. The dependency is verified before the program runs.

Comments work on a different model. A comment that says “X provides Y” is a note from one developer to another. It carries information about intent. It doesn’t run. It doesn’t check. It doesn’t fail when X stops providing Y. It sits there saying “X provides Y” indefinitely, long after Y has gone missing, because nobody told the comment that the overlay Dockerfile had a gap.

This is the core problem with moving a dependency from explicit code to implicit documentation. Explicit dependencies — import statements, function calls, direct references — have enforcement mechanisms. The language, the compiler, the linker, the runtime loader: something verifies the dependency before execution. Implicit dependencies — comments that say “the other thing handles this”, README sections that describe what the sidecar does, migration scripts that assume the previous one ran — have only documentation, which is to say, nothing.

The pattern that fails

It shows up everywhere that systems are decomposed into layers or artifacts that modify each other:

  • A Dockerfile base image installs a component; the derived image assumes it’s configured correctly without checking.
  • A Kubernetes Helm chart deploys a service and a ConfigMap; the service’s startup expects a key that the chart template forgot to add.
  • A plugin system has a registration file; extracting a plugin into a separate package works fine until someone removes the registration entry and writes “new package handles this.”
  • A migration sequence has a step that depends on the previous step having run; there’s a comment saying the previous step is required, no enforcement.

In each case, the author of the comment knew something true at the time they wrote it. The comment was accurate. The gap was that the thing the comment described — the overlay, the package, the sidecar, the prior migration — was a separate artifact with its own evolution, its own Dockerfile, its own deployment pipeline. The two things can drift independently. Comments don’t get a pull request when the artifact they describe changes.

The result is always the same: the feature works until it doesn’t, without a clear signal that it stopped working, often without any signal at all.

Shift left

The principle that makes this a solvable problem, not just an inevitable one, is old enough to have become a cliché: push verification as early in the pipeline as possible1. Every stage where a contract can be checked is a stage where it should be checked. The earlier the check, the shorter the gap between the lie and its discovery.

The hierarchy looks like this:

Compile time — The compiler rejects code that calls nonexistent functions. You get this for free in any typed language. There’s no lag: the dependency is verified before you ship.

Build time — The CI pipeline can run checks that don’t fit in a compiler: format validation, integration tests, custom scripts that verify configuration assumptions. You pay the cost of writing the check once, and it runs on every commit.

Deploy time — Startup scripts, init containers, migration validators. These fire after the artifact is built but before traffic reaches it. Still fast feedback, but later than build time.

Runtime — The feature silently doesn’t load. You find out when someone notices the silence.

The comment that said “overlay provides this” was a runtime dependency treated as a comment. The fix was to move it to build time.

What the fix looked like

A small script added to the overlay’s build process. Idempotent: it checks whether the module name is present in the profile’s allowlist, and adds it if it isn’t. Explicit failure: if the regex that locates the allowlist doesn’t match — because someone refactored the configuration format upstream and the overlay script is now looking for something that no longer exists — the build fails loudly.

# fails the build if the patch target moves
if ! grep -q 'expected_pattern' config_file; then
  echo "ERROR: expected_pattern not found — upstream layout changed"
  exit 1
fi

That explicit failure is the point. The comment said “overlay provides this” and was silent when it stopped being true. The script says “I am verifying this contract” and is loud when the contract breaks. The contract is now enforced at build time — the image cannot be pushed if the module name isn’t in the allowlist.

This pattern has a name in type-theory-adjacent literature: making illegal states unrepresentable2. You design the system so that the invalid state — module code present, module name absent from allowlist — cannot be produced by the build process. The script doesn’t allow the bad image to exist. If it tries to, the build stops.

The broader shape of implicit dependencies

I keep hitting this in different contexts. A README that says “run migrate.sh before deploying.” A Makefile with a ## prerequisite: build must have run first comment. A workflow with steps that silently succeed even when upstream steps produced empty output.

In each case, there’s a fact that someone knew to be important and chose to express as documentation rather than enforcement. The documentation is fine when the system is small enough for all the relevant facts to stay in someone’s head. It stops being fine when the artifact that owns the dependency has its own independent deployment lifecycle.

The rule I’ve landed on: if someone would need to read a comment to understand a dependency, that dependency should probably be a check. If the check can happen at build time, it belongs there. If it belongs at deploy time, it should hard-fail, not warn. If it belongs at runtime, it should produce a clear, immediate error — not a silent absence.

Comments describe what you intended. Checks verify what you actually built. When those two things are different, only one of them tells the truth.

— Pete


  1. The “shift left” principle — moving testing and verification earlier in the development pipeline — was articulated in software engineering literature in the early 2000s and is now standard in both security (SAST, DAST) and quality assurance. See: IBM Systems Sciences Institute research on defect cost multipliers across development phases; the earlier a defect is found, the cheaper it is to fix. For testing specifically: Michael Cohn, Succeeding with Agile (2009) and the “test pyramid” model. 

  2. Yaron Minsky, “Effective ML Revisited”, Jane Street Tech Blog, 2014. The principle “make illegal states unrepresentable” — design data structures and system configuration so that invalid states cannot be expressed, let alone reached. Originally from Minsky’s OCaml talks but has become foundational across typed functional programming communities.