Skip to content

v1.27.1 ·

The plus sign that meant two things

The problem

A refresh command rewrote nineteen files and said nothing about what it had taken away.

The report described it precisely. Compiled phase skills are build artifacts, regenerated from project context documents on every refresh. They are also, in practice, the only place hand-authored architecture and style prose survived — so authors wrote into them, and an unrelated refresh months later destroyed the writing with no diff, no warning, and no way to opt out. Adding one guidance layer, a one-line edit, was enough to trigger it.

Four things made it hard to catch, and each is a separate defect.

The dry run showed filenames, not content. Every changed file was marked with a plus sign, and the same plus sign meant created and replaced your prose with older prose. There was no legend, no line counts, and no diff. A careful operator who previewed first still could not see that text was about to be deleted.

The file list double-counted. Twenty-nine marked lines were printed for nineteen actual files, because each canonical skill was listed once per configured platform. The inflated list made the real blast radius harder to read rather than easier — and the number that mattered, how many distinct files were about to change, appeared nowhere.

The success report was silent about loss. Nineteen updated, ninety-three unchanged, and nothing distinguishing "refreshed to identical bytes" from "replaced what you wrote with something older."

And the health check agreed everything was fine. It ran as part of the same command and reported nothing to do, while the context documents were six commits behind the code they described and the committed artifacts disagreed with the sources they were built from. Both are conditions a health check is the natural place to notice, and it was looking at neither.

The loss was not recoverable by re-running anything, because the better text existed nowhere but version history — and every subsequent refresh destroyed it again. In the reporting project the regenerated text asserted the codebase contained no product runtime and no cloud resources, four commits after a verified deployment, and replaced a real style guide with auto-detected file-count percentages plus one convention that was simply wrong. Agents read these files during a phase, so the downgrade landed exactly where it misleads most.

Underneath all four is one property: the operation was silent. Not too aggressive, not incorrect about which files it owns — silent. It did what it was designed to do and gave nobody the information needed to stop it.

How it could be solved

The report arrived with its own five fixes, ranked by the person who had just been bitten. That is a better starting point than a blank page, and it was still the wrong list — two of its premises did not survive being checked, and checking them was most of the design work.

The first premise was that hand-authored context had nowhere to live. It already did. Four hours before the tag the reporter was running, a fix had landed that makes regeneration preserve any context doc whose frontmatter no longer claims a generator wrote it. Remove the marker, and discovery leaves the file alone. So the option of building a home for authored prose was already spent; what remained was that nothing anywhere said so, which is a much cheaper problem and a different one.

The second premise was that this repository had lost prose to the bug. It had not. The style guide the report described as replaced had been three hundred and four bytes in every commit it ever had, and the sentence quoted as evidence appears nowhere in this history, on any branch. The reporter was a downstream project. There was no restoration to perform — which turned a recovery task into a writing task, and a much smaller one.

What was left was the detection half, and there the real choice was where a divergence check should get its truth from.

The obvious option was to recompute: rebuild what the compiler would emit and compare it to what is on disk. Correct, and rejected on a fact about the import graph. Rebuilding the plan reaches the knowledge engine, and the knowledge engine reaches Node's sqlite binding, which is still tagged experimental and prints a warning the first time it is loaded. Every routine health check would have carried that warning, on every project, forever. The same hazard is already documented in the migration next door, and pinned there by a test, because a behavioural test would keep passing until somebody happened to exercise the path.

The option taken instead was to make each artifact describe itself: the version that emitted it, and a hash of the prose beneath. The check becomes a file read. It also buys something the recompute never offered — the ability to tell a framework upgrade apart from a human edit, which matters because the remedies are opposite. An artifact emitted by an older version wants refreshing. An artifact edited at the current version wants somebody to decide whose text is right, and refreshing it is the loss.

A third option was rejected on this project's own history: prompting before every overwrite. The command that does the refresh has no unattended mode by design, and adding a question to it would trade a silent overwrite for a hung pipeline. Confirmation went to the one command whose overwrite is genuinely unrecoverable and which a person always runs by hand.

The last option was not to build the fifth fix at all, and that is what happened. Splitting the framework upgrade from the project-content re-inline is real work with a real case, and it was filed as a backlog item that argues for dropping itself: most of the harm here was that the rewrite was silent, not that it was atomic, and a new flag is a cost every future run pays. Making an operation legible is cheaper than giving it a second mode.

How AIDLC solves it

The reported defect was a silence: aidlc update --project-only rewrote 19 files with no diff, no warning, and no way to opt out, and aidlc doctor called the repo healthy while the committed artifacts disagreed with their sources. This ships the detection and reporting half.

  • Per-file change records with create/overwrite kind and line counts, on both report types, from one shared producer so dry and real runs cannot disagree.
  • One reported line per distinct path — 57 for 57 on this repo, down from 75.
  • A self-describing marker on every generated body: emitting version, a hash of the body, and the two places to edit instead.
  • Two report-tier doctor migrations: artifact-source-drift and context-doc-stale.
  • aidlc discover --force confirms before destroying a hand-owned context doc, and declines when it cannot ask.
  • This repo's .aidlc/context/style-guide.md, written from measurement.