DEV Community

Lex
Lex

Posted on

git blame Told Me I Wrote 767 Lines I Didn't Write

git blame Told Me I Wrote 767 Lines I Didn't Write

Before writing about a gate, I checked who wrote it.

The gate is 767 lines of JavaScript that refuses to ship a generated document when the model asserts a number or a tool that isn't backed by the source files. It sits in a document-generation pipeline I run locally — the same source material, retargeted per audience, which is exactly the setup where a model starts filling gaps to fit. I wired the gate into that pipeline four days ago, watched it abort real output the same afternoon, and decided it was worth a write-up.

So I ran git blame. Seven hundred and sixty-seven lines out of seven hundred and sixty-seven: me. Two commits in the log, both mine. Clean history, single author, no ambiguity.

I didn't write a single line of it.

Why the blame lied

The repository is a fork. origin points at an upstream project — MIT licensed, someone else's — that ships an updater for its own system layer. I run a command; it fetches the canonical files and writes them to disk.

That's how this file arrived. When I committed it, git recorded the only thing it can record: who moved the bytes. The commit message says as much — it's an automated system-files update, and nothing else — and the blame still puts my name on all 767 lines.

git blame does not answer "who wrote this". It answers "which commit touched this line last, and who signed that commit". Those are usually the same answer. In a fork with an auto-updater they stop being the same answer, and the blame has no way to tell you that.

Which is a shape I've written about before. A prose note asserting that a setting was off. A blame asserting that I wrote a file. Both authoritative, both mechanical, both wrong — except the note was prose and the blame is a tool, and the tool is far more convincing.

Who did write it

The authorship lives in the upstream history, not mine — named there, not here. Blame the file against origin/main and three contributors come back:

Contributor Surviving lines
A 301
B 246
C 66

A fourth touched the file early on; none of those lines survive in the current blame.

The shape of that history matters more than the split. The module started as a validator for invented numbers, and was extended weeks later to cover asserted names as well as numbers — the things a document says exist, not just how many. Then came the corrections, and the corrections are the interesting part. One trigger was case-sensitive in a way nobody intended, so a whole class of claims walked past untouched. In several languages the checker silently found nothing at all. One internal heuristic had drifted into answering a different question than the one it was built for. And a pattern was dropping claims on the floor without reporting it. Four people, roughly a month, five separate ways for a fact-checker to be quietly wrong.

I would not have built that. I'd have built the version that works against my own text and fails silently against someone else's — which is the same version, right up until it isn't. The quality in this module isn't in the original design; it's in the corrections. None of the corrections are mine.

What is mine

Here's the part that surprised me more than the blame did: upstream never calls the gate.

The module ships as a library and a CLI, and nothing in the generation path invokes it. The engine existed; the barrier did not. My commit wires it into three call sites along that path — 197 lines, on my branch only, not an ancestor of upstream's main. Before it, the gate was something you could run. After it, the gate is something the output has to get past.

The call site is nine lines. Symbol names changed for privacy, structure untouched:

const result = checkClaims(text, { sources, config, cwd });

if (result.verdict === 'block') {
  throw new Error(`blocked: ${describe(result)}`);
}
Enter fullscreen mode Exit fullscreen mode

No retry at this point, no warning mode, no flag to skip it. The verdict comes back and the document either gets written or it doesn't. Nine lines is the whole of my contribution to the enforcement itself — the other 188 are plumbing: finding the source files, threading the config path, deciding which of three generation steps each check belongs to.

The second thing that's mine is a config file: the allow and deny lists the engine reads. Verified figures the generator is permitted to state, phrases I never want appearing in generated text. The engine is deliberately generic — it knows how to ask whether a claim is supported, and nothing about what it's checking. That file is where it learns.

The honest name for that work is integration, not authorship, and the distinction is worth keeping because the failure modes differ. The authors get it wrong when the gate mis-detects. I get it wrong when the gate doesn't fire. A perfect gate nobody wired is a file.

The first thing it blocked was true

The document was a technical write-up about my own agent system. The gate aborted it, and the reason it gave was a list of four asserted tools it could find no backing for:

  • planner-worker
  • orchestrator-executor
  • artifact-based handoffs
  • role enforcement

My first read was the obvious one: the model invented four tools. It does that. I've watched it do that. I went and checked anyway, because the blame had just taught me what assuming costs.

It invented nothing. git grep across my agent system returns zero literal matches for all four strings — and every one of the four concepts is real and documented there. Planner and implementer are roles in the roster. Root-driven orchestration is what the architecture became after the orchestrator agent was absorbed into root. Artifact-based handoffs are how deliverables cross between agents: plans, reports, checkpoint files on disk. And role enforcement is a deny-by-default hook — the one I wrote my first article about.

So the model described my architecture correctly, in vocabulary that doesn't appear in the source files it's allowed to draw from. It didn't invent facts. It invented synonyms. The gate cannot tell the difference, because from where the gate stands there is no difference: an assertion with no backing text behind it is an assertion with no backing text behind it.

And the gate was right. A gate that accepts "true, but not in the sources" is a gate that negotiates, and a gate that negotiates isn't a gate — it's a suggestion with extra steps. The cost of blocking fabrications is blocking unreceipted truths. That's the trade, and I'd make it again.

Three days later, it lied properly

A different abort, different class. This time the gate reported an invented figure — a count with no counterpart anywhere in the sources — and added the part that matters: the model had produced the same drift once already, been corrected, and produced it again on the next pass.

No synonym this time. The number came from nothing.

Which sent me looking at how that kind of drift had been handled before the gate existed. I found it in the generation code: a correction written by hand into the prompt, naming one specific figure and the wrong value the model kept reaching for. One fact, pinned in prose, inside the instructions.

It's the same move I wrote about in August — a value living in a prompt string instead of coming from a source — and it's the reason this gate needs to exist at all. That line only ever protected one number. Every other figure the model might drift on had nothing standing behind it until something started checking all of them.

That's a pattern I've hit before. In an earlier piece I described a claim the model kept inverting; fixing the wording at the source reduced the problem and did not remove it, and it took a hard stop downstream to close. Same shape here. A correction is a suggestion the model weighs against the task. A gate is a fact about the world.

Two aborts, three days apart, and the useful thing is that the gate treated them identically. It does not distinguish true from false. It distinguishes supported from unsupported. That makes it blunt in the first case and exactly right in the second — and since it cannot know which case it's in, treating them the same way is not a flaw in the design. It is the design.

Both documents stayed on my machine. Neither had to be caught by a human reading carefully at the wrong hour, which is the review process the gate replaced.

Both stories are the same story

A tool that cannot verify what it asserts will give you the answer with exactly as much confidence as one that can. The blame said I wrote 767 lines. The gate said four real tools weren't real. Neither of them lied. Both of them answered a question slightly different from the one I asked, and neither had any way to flag the gap.

The difference between them is that the gate declares its method. It blocks for want of backing and says so: unsupported, not false. The blame hands you a name and a date and never tells you what it is accountable for. A tool that documents its own limit is usable. A tool that doesn't will mislead you without ever being wrong.

Three pieces in, this keeps being the same lesson at different altitudes. Prose rules don't survive contact with a loop, so enforcement has to live one layer down. Prose that describes configuration is configuration, just without a type checker. And now: tools that summarise the past are making claims about the world, and those claims go stale in exactly the way prose does. git blame is a derived artifact with a fossilised assumption inside it — that whoever committed a line is whoever wrote it.

What I actually do with this: before asserting anything a tool handed me, I ask what question that tool answers, not what question it appears to answer. That's the same move the gate makes on every document it sees. The difference is the gate does it every time, and I have to remember.

Limits

The gate has been wired for four days, and everything above comes out of that window.

Three aborts carry a verbatim message in the logs. Counting the narrative entries in my own daily notes it's around six, but those overlap with the logs and I can't cleanly separate them, so six is a bound and three is the count I'd defend. There is no aggregate counter anywhere. I tallied these by hand, which is its own small irony in a piece about trusting derived artifacts.

The split: two aborts for an unsupported tool, three for an unsupported number. Zero for the blocked-phrase list, which is configured, loaded, and has never once fired. I mention it because it's the part of the gate that has demonstrated nothing, and a reader looking for the weak joint should be handed it rather than have to find it.

One operator, one pipeline, one month of upstream history I didn't write. No control group, and no idea whether any of this survives contact with a setup that isn't mine.

What I'd do differently

The config file — the one that teaches a generic engine which figures are verified — is gitignored. No history. No blame. Not even the wrong blame. If a figure in that allow-list turns out to be stale, nothing on disk can tell me when it went in or what it was checked against.

Which is the thing I wrote about in August: configuration living somewhere no tool would ever look. I published that piece, and roughly a month later populated this file outside version control without noticing. Knowing the failure mode is not immunity from it, and I'd like that to be less funny than it is.

So: version the file, local branch if nothing else, and write down where each allowed figure comes from. Right now an entry asserts that a number is fine and doesn't say why — a frozen value with no provenance, which is precisely the shape that bit me last time.

The open problem is bigger than the file. The gate distinguishes supported from unsupported, which ties it to whatever is already written in the sources. A genuine tool that isn't documented there is indistinguishable, from where the gate stands, from one the model made up. The answer isn't to loosen the gate — it's that the sources are where that gets fixed, which means someone has to keep them current. I haven't solved who, or how often, and four days of data doesn't tell me.

I started by checking who wrote the gate because I was about to assert it in public and I wanted the assertion to be true.

The check took twenty minutes and cost me the article I meant to write.

The gate demands a source before it lets a claim through. I ran the same check on my own. That's the whole of it.

Top comments (2)

Collapse
 
reidmarlow profile image
Reid Marlow

The part that resonates is treating the gate as a fact about the world rather than an advisory check. Once a generator knows it can negotiate or explain away an ungrounded claim with a prompt retry, you end up maintaining a fuzzy prompt wall forever. Halting on zero backing text is blunt, but at least the failure mode is loud and reproducible.

Collapse
 
raju_dandigam profile image
Raju Dandigam

The distinction between the byte mover and the person who made the change operative is excellent. For generated artifacts, I’d preserve the upstream input, generator version, and policy/gate result alongside the commit; otherwise blame can identify the change but not reconstruct why those 767 lines appeared. Would you make that provenance record part of the gate output itself, so a future reviewer can reproduce the exact decision?