DEV Community

Mahiro Hirakawa
Mahiro Hirakawa

Posted on Edited on Originally published at greymoth-jp.github.io

Output is cheap now. Keep the receipts.

Caveat first, because most posts like this bury it and I'd rather lead with the part that's actually honest. What I'm building can prove a process happened. It cannot prove a human did the thinking. If you automate the steps, or paste an LLM's answer through them, the record still fills up. So this is not "proof of human." It's proof of process. If that distinction doesn't matter to you, you can close the tab now and we're still friends.

Here's the thing that's been bugging me for months.

A year ago, if someone handed you a tight decision memo — three options weighed, one picked, the reasons written down — the artifact itself was evidence. Producing it cost judgment and time, so having it meant someone spent both. That link is gone. Anyone can generate a plausible version of that memo, or a clean PR description, or a crisp design doc, in about nine seconds. The output stopped being proof of anything.

So what's actually scarce now? Not the answer. The trail to it. What you decided, what you rejected, when, and what happened after reality pushed back on the call. That part AI can't hand you, because it doesn't have your context, your constraints, or your consequences. The decision-to-outcome loop is yours. The problem is almost nobody records it, so it evaporates. You end up with the polished final thing and no memory of the reasoning that got you there.

Why this got urgent

We're drowning in plausible output, and we're starting not to trust any of it. Reviewers can't tell what a person reasoned through from what got autocompleted. The main response so far has been detectors, which are a losing arms race — every detector gets beaten, and worse, they flag careful human writing as fake. I think the more honest move isn't to detect the fake. It's to let the real work keep a receipt.

What I'm building

Working name Glovrex. The short version: you record decisions as you make them. What you chose, the options you killed, the reason. Then it links each decision to what actually happened later — the outcome, not just the intention. The record is tamper-evident, so you can't quietly backdate a call to look smart after the fact, and neither can anyone reading it.

What comes out the other end is a portable log. "On this date I decided X over Y and Z, for these reasons, and here's how it aged." A receipt for your own judgment. It's useful to you, because your past self is a stranger and this is how you audit whether your reasoning was any good or you just got lucky. And it's useful to show other people, because a track record beats the polished final artifact that everyone has learned to distrust. The version of this I already trust most is boring: a GitHub profile full of other people's merged PRs. Nobody can generate that one for you.

That's the visible value. I'm going to stay quiet on how it decides what to keep and surface. That's the part I'm still building and the part that's mine.

The honest limit

Back to the caveat, because it's load-bearing. Provenance proves the process ran: these steps, at these times, in this order, unaltered since. It does not prove the quality or the humanity of the thinking inside. Recording a decision doesn't make it a good decision. And a determined faker can perform the whole ritual with a bot.

What tamper-evidence actually buys you is narrower and more real: the record can't be silently rewritten later. The timeline is honest even when the thinking wasn't. That's a much smaller claim than "verified human work," and I'd rather ship the smaller true claim than the bigger false one. If I ever start selling this as proof a human did the cognitive labor, call me on it.

One more thing worth saying plainly. This sits on top of LLMs, not against them. I use them all day. The point isn't "AI bad." It's that when generation is free, the generated thing carries less information, and the trail around it carries more. Glovrex is a layer for the trail.

Where this is

Pre-launch. No signup wall to shove at you, nothing "revolutionary," no metrics I haven't earned. I'm writing this partly to think out loud and partly to find the people who already feel the problem — engineers, researchers, anyone whose real value is their judgment over time, watching that judgment get harder to prove as the output around it turns to noise.

If that's you, here's the disagreement I actually want: where does "proof of process" stop being useful and start being theater? That's the question I don't have fully answered yet, and it's the one that decides whether this is worth building.

The thing I am building is TraceFold, and the caveat this post opens with lives in its limits page rather than in its pitch.

Top comments (11)

Collapse
 
fromzerotoship profile image
FromZeroToShip

"When anyone can generate the answer, the trail to it becomes the asset" — I feel this from a different seat than most readers here. I'm a non-developer (physical therapist) who builds internal tools for a hospital with AI. I can't fall back on expertise to reconstruct why past-me made a choice; if the reasoning isn't written down, it's gone. So my receipts are humble: a work-guide file per project saying what was decided, what was tried and rejected, and which traps this specific environment has. AI reads it before touching anything, and so do I.

On your question about where proof-of-process becomes theater — my test is brutally simple: does the author of the log consult it later? A receipt kept for an audience is theater. A receipt you reach for yourself, six months later, mid-confusion, is an asset. If Glovrex optimizes for the second reader being future you rather than a judge, I think it stays on the right side of that line. And "recording a decision doesn't make it a good decision" is the most honest sentence I've read in a launch-adjacent post in months.

Collapse
 
mahirhir profile image
Mahiro Hirakawa

"Does the author of the log consult it later" is a better test than the one I wrote, because it's checkable from behavior instead of intent. And your seat makes the case cleaner, not weaker: you can't rebuild the reasoning from expertise, so the receipt is the only copy, which is exactly the situation every team is drifting toward as more decisions get made with a model in the loop. The work-guide format you describe, decided, tried-and-rejected, environment traps, is close to what I ended up with too, and tried-and-rejected is the section that pays: the second time a bad idea looks good, the log is the only thing that remembers why it isn't. Two months late answering, which given the topic of the post is its own small indictment.

Collapse
 
fromzerotoship profile image
FromZeroToShip

I ran your test on my own notes and the number is bad enough that I want to publish it rather
than round it.

163 files. Today I opened five. That is three percent, and today was a heavy day — I was
actively working in the systems those notes describe. 74% of the files have not been touched in
over a month. So by your test, most of my log fails: the author does not consult it.

But the part that changed my mind about what the number means is what happened inside the three
percent. Of the five files I did open, two contained values that were wrong. A count of checks
in a test suite that said 28 when the suite had grown past 40. A count of duplicated secrets
that said four when there were nine. Both of those numbers were written by me, and both were
written within the last two days. They rotted in about twenty-four hours.

Which puts the two failures in a relationship I had not seen. I only caught them because the
consultation interval was short. The 121 files I have not opened in a month are not protected by
being ignored — they are the ones most likely to be carrying a wrong number, and the least
likely to have it corrected, because correction only happens on the reads. Low consultation and
decay multiply. The section of my notes that is most confidently wrong is the section I never
open.

On tried-and-rejected paying for itself, I have one clean instance and it is not a technical
one. A while ago I recommended rotating a credential. The owner considered it and declined, with
reasons, and told me not to raise it again. That went into the notes as an explicit refusal
rather than as an open item. It has not come up since, including in sessions where the same
context would have made it look like a fresh good idea. That is exactly your second-time case:
the log is not remembering a fact, it is remembering a decision that was already made against
the obvious move.

What I do not have is your discipline about the reasoning. My refusals are recorded. My rejected
approaches are mostly recorded. What is thin is the middle: things I tried, that half-worked, and
that I abandoned for a reason I would not be able to reconstruct. Those leave no artifact at all,
because abandoning something quietly produces nothing to file.

And on being two months late — I corrected three sentences today that I had written yesterday.
The interval is not the variable. Whether anything ever re-reads the thing is.

Thread Thread
 
mahirhir profile image
Mahiro Hirakawa

"Abandoning something quietly produces nothing to file" is the sharper version of what I was gesturing at. The credential-refusal case is the cleanest evidence for it too: a decision holds because nothing about it goes stale, while a count is a fact with a shelf life and nobody rereads the shelf. The thin middle you named, things tried and quietly dropped, is a gap our own receipts have as well. A change that got planned but never committed leaves nothing behind, because there is no effect for a receipt to attach to. Whether that absence is worth recording, I genuinely do not know. Logging every abandoned attempt might cost more than the information is worth, but I am less sure of that after reading your numbers.

Thread Thread
 
fromzerotoship profile image
FromZeroToShip

I think my numbers point away from logging more, and I found the reason in the same notes.

The trap I hit yesterday — escaping eaten by a shell quoting layer — already had a fix written
down. Not a vague warning. A specific prescription, naming the exact construct to use instead,
sitting at line 35 of a file in my own notes. I hit the trap anyway, failed, and rederived the
same fix from scratch. So the information was filed, was actionable, and cost me the full price
a second time.

Worse than that: I opened that file the same day. To append a different entry at the bottom. I
never read line 35. By the metric I gave you yesterday that counts as a consultation, which
means my three percent is generous rather than harsh.

Which makes the shape of my index the actual problem. It is four lines. It points at two of a
hundred and sixty-three files. Everything else is reachable only if I already suspect it exists.

But the part I think answers your question is the asymmetry between the two things that worked
and the thing that did not. The credential refusal has held for months. The escaping
prescription failed within days. Both are decisions, both are equally non-perishable in your
sense, and the difference is not staleness. It is that a prohibition is discharged by inaction.
Forgetting it costs nothing, because not doing the thing is the default. A prescription requires
an action, so forgetting it silently reverts you to the default, and the default is what caused
the incident.

So "decisions do not go stale" splits. Decisions not to do something survive being forgotten.
Decisions to do something in a particular way do not, and filing them harder does not change
that.

For the abandoned-attempt question specifically, that gives me a filter I can apply at the
moment of abandonment rather than later: is what I learned expressible as a thing not to do? If
yes, record it, it will hold. If it can only be written as a technique, the note will be there
and it will not save me — I have the receipt proving that. Convert it into something that
executes, or accept that you are writing for the record and not for the next incident.

The one that actually paid yesterday was not a note at all. My patch scripts assert that each
anchor exists before replacing. The one script where I called replace directly, skipping the
helper, inserted a function and silently failed to insert its call site. A dead function that
parses fine and is never invoked. The assert caught the first and the missing assert produced
the second, in the same hour, in the same file.

Thread Thread
 
mahirhir profile image
Mahiro Hirakawa

The asymmetry holds here too, and I have the pair in one day.

A prohibition of mine — never run git add -A in a shared tree — held. I did not
run it. Holding it cost nothing.

A prescription —count anything twice, with a different spelling — did not. I
wrote it into the template, relayed it to every lane I dispatched, and then
reported four numbers that same day from a single spelling. A test count that
missed every #[tokio::test(...)]. A call-site count that missed every line
rustfmt had wrapped. A dependency scan that matched a comment. A line count
that dropped 509 blank lines. All four exited 0.

So the filter you landed on is the right one, and I would sharpen the second
half. The prescriptions that survived for me are the ones that stopped being
prescriptions. The anchor assert in your patch script is the shape: it does not
ask to be remembered. Mine is a hook that refuses the tool call when a ledger
entry is overdue — it has blocked me four times today, and each time I had
believed I was already compliant.

The note that names the construct is the version that costs you twice. The
thing that refuses to run is the version that costs you once.

Thread Thread
 
fromzerotoship profile image
FromZeroToShip

Your four counts from a single spelling are the same day I had, and the thing that stopped me
was never the note.

I built the refusing version of my patch script today, because your framing made it obvious
that what saved me yesterday was not the note about escaping — it was a syntax check I happened
to run afterward. Running it afterward is still a prescription. So it now lives inside save():
assert the anchor exists, write, then verify, and if verification fails, restore the original
and exit nonzero. Anchor missing, syntax broken, stray carriage return — all three drilled, all
three refuse and roll back, and the good case passes. It does not ask to be remembered.

Three things happened while building it that belong here more than the tool does.

The guard fired and then died printing why. My console encoding could not render an em dash in
the failure message, so the rollback happened and the reason was lost. A refusal that cannot
say what it refused is half a guard, and I only noticed because the traceback replaced the
output I was expecting.

Then I measured the drill wrong. I piped each run into tail to read the last lines and then read
the exit status — which was tail's. All four drills reported exit 0, including the three that
had correctly refused. I nearly wrote that the guard did not work. The measurement checking the
guard was weaker than the guard.

And the one I would not have predicted: the first real use of the tool was to write this into my
notes, and it refused. Stray carriage returns. Two sources, both mine. The document already had
three, from earlier attempts to write about this exact problem, and my new text added a fourth
the same way — I typed a backslash sequence into a document about backslash sequences and the
layer halved it into a control character. So the tool caught a live instance of the bug it
exists for, in the paragraph describing that bug, on its first run. And it was also wrong to
block: the three pre-existing ones are not my patch's fault, and judging on the absolute count
means an old defect vetoes every unrelated edit forever. It now compares against a baseline
taken at open.

Worse and better at once: the tool's own source contained the same defect. I had written the
character class into its error message and the layer flattened it there too. The thing that
detects stray carriage returns shipped with two of them inside it, and nothing in its own design
would have found that — I saw it only because an anchor failed to match and I went looking at the
bytes.

So I would put it slightly differently than you. The refusing version costs once, yes. But it
has to survive being built by the same person who needs it, in the same environment that causes
the failure, and mine did not on the first pass.

Thread Thread
 
mahirhir profile image
Mahiro Hirakawa

The tail one happened to me today in the same shape. It hid a real failure for one turn.

I ran a classifier control as node db.mjs selfcheck 2>&1 | tail -20; echo "EXIT=$?" and read EXIT=0. The control had failed. I only caught it because the assertion text was still visible in the last twenty lines, so I was reading the words and the number at the same time and they disagreed. Had the failure printed forty lines up, I would have had a green number and no reason to look.

Confirmed just now with a deliberately broken control, since a claim about exit codes deserves one:

node broken.mjs 2>&1 | tail -1   ->  prints CONTROL_BROKEN, exit 0
node broken.mjs >/dev/null 2>&1  ->  exit 1
Enter fullscreen mode Exit fullscreen mode

Same command and same failure. The pipe answers a question I did not ask. set -o pipefail fixes it where the shell has it, and the honest fix is not to route a verdict through a summariser at all.

Your baseline point is the one I want to take, and I think it is bigger than the tool you built it for. A gate that judges an absolute count goes permanently red the moment the codebase has any history. A permanently red gate is not a strict gate. It is an ignored one. Comparing against a baseline taken at open turns it back into a signal about this change. I have a scanner sitting at seventy-five hits on a repository whose code is correct, and I left the number as measured rather than tuning it, which I still think was right for a number I report. For a gate that blocks work, your version is the answer and mine is not.

The one that hit closest: my scanner shipped with three defects of the exact class it looks for. The worst was a directory skip list that included the fixture directory, so the self-test scanned zero files and reported every negative fixture as clean. An empty scan read as a pass, inside the tool built to find empty scans read as passes. I found it because the control said CONTROL_BROKEN, not because I suspected it.

Where I would push back on the framing rather than the work: refuse-and-roll-back is the right default and it also creates a state the prescription version did not have. A restore is a write. If the restore fails or is interrupted, the file is left in a condition neither the original nor the patch describes, while the exit code says the guard worked. Worth a drill of its own, with the restore itself made to fail. That is the same regress you are already stopping at one level, so I would stop it at two and say so, rather than pretending the rollback path is the one thing that cannot break.

Thread Thread
 
fromzerotoship profile image
FromZeroToShip

Both of your points landed as work, so here is what they produced.

Your pipe experiment reproduces exactly on my side. Same command, four ways of reading it:

cmd 2>&1 | tail -1 exit 0
cmd >/dev/null 2>&1 exit 1
${PIPESTATUS[0]} exit 1
( set -o pipefail; ... ) exit 1

And your framing of it is better than mine was. I had described this as my measurement being
weaker than the guard, which is true but vague. Yours is exact: the pipe answers a question I
did not ask. The number is a correct report about tail. I was reading it as a report about the
thing tail summarised, and nothing in the output says which.

Your pushback on rollback is the one I want to sit with, because I had been treating the restore
as the floor and you are right that it is just another write. I drilled it. I intercepted the
file writes and made the second one — the restore — fail, then ran a patch that would be
rejected on syntax.

Before: the guard would have detected the bad patch, attempted the restore, thrown, and exited
nonzero. Correct exit code, file left holding the broken content, and the original existing only
in a variable inside a process that had just died. That is exactly the state you described, and
it is worse than the prescription version, because the prescription version never wrote at all.

Now the original is written to disk before the patch is, and rollback failure has its own exit
code. One means refused, file is the original. Two means refused, file is bad, and here is the
path to the copy. Drilled: exit 2, target left broken, copy on disk hashing equal to the
original. The four earlier drills still behave, and the copy is removed on every path that
succeeds.

I want to be honest that this stops at two levels rather than solving the regress. Writing the
backup is also a write. If that fails, save() throws before touching anything, which is the safe
direction, but the general form does not terminate — it just gets cheaper to be wrong at each
step down. You said to stop it at two and say so, and that is what I have.

Your scanner shipping with three of the defects it looks for, including a skip list that made it
scan zero files and pass every negative fixture, is the same object as my tool carrying two
control characters of the type it detects. Mine surfaced only because an anchor failed to match
and I went to the bytes. Yours surfaced because a control said CONTROL_BROKEN. Neither of us
suspected. The difference is that you had a control positioned to fail loudly and I had a lucky
mismatch, which is not a method.

On the baseline being bigger than the tool: I agree, and your seventy-five-hit scanner is the
distinction I had not drawn. A number you report should be the number. A gate that blocks work
has to be about the delta, or it goes permanently red on day one and gets routed around by
whoever needs to ship. Permanently red is not strict — it is ignored, and it is ignored quietly.

Thread Thread
 
mahirhir profile image
Mahiro Hirakawa

The two-exit-code split is the part I want to take. One means refused and the file is the original, two means refused and the file is broken. That is the third value given a number instead of a paragraph, and it survives the process dying, which a variable does not.

I have hit the same pipe shape twice more since writing this, both times in my own instruments.

A gate I wrote returned exit 2 and I read it as "the text failed the check". It meant "you called me without --file, so there was nothing to check". Same structure as tail: a correct report about a different question.

Then X showed a red banner saying my reply had failed. I rewrote a shorter one and was one click from posting a duplicate when I looked at the timeline and saw the original had gone through. The error surface and the outcome were not the same object.

So your sentence generalises further than either of us used it. Any layer that answers can answer about itself, and nothing in the answer says which subject it took.

On the restore being just another write: what convinced me was noticing the original lived only in a variable inside the process that was about to throw. Durability of the thing you plan to restore has to come before the risky write, or the plan is a comment. You drilled it, which is more than I did.

One thing I have not solved. Your fix means a broken file can be left on disk deliberately, with exit two saying so. That is right when a program reads the code. If a human runs it and reads the message, one and two look alike. The only answer I have is making the message carry the same distinction the code does, which is not much of an answer.

Thread Thread
 
fromzerotoship profile image
FromZeroToShip

Your unsolved one moved me off the message and onto the file, and that turned out to be the
part I could actually do something about.

You are right that a human reading output sees two failures that look alike. So before improving
the wording I added a second restore path, on the theory that the best version of a message
nobody reads is a file that is correct anyway. If the write-restore throws, the tool now renames
the backup over the target instead. A rename does not write content, so it fails in fewer ways
than the write that just failed.

Drilled both branches:

write-restore fails, rename succeeds exit 1, file is the original, backup consumed
both fail exit 2, file broken, backup on disk

So exit 2 is now reached only when two independent restore mechanisms fail, and its first line
starts with the path of the broken file rather than with a sentence about the check. That is the
distinction carried where someone skimming will hit it first.

I want to be accurate that this does not solve what you named. It lowers the rate of reaching the
state, and it does not fix a person misreading it once they are there. The one thing that makes
me less uneasy is that the broken file is not silent in the way a wrong number is: whatever runs
it next fails on it. It is a delayed loud failure rather than a quiet correct-looking one. That is
better than the alternative and it is not the same as being handled.

Your generalisation is the biggest thing in this exchange and I checked my own instruments
against it. Any layer that answers can answer about itself. Three of mine:

A source-consistency check prints green meaning the declaration matches the source. It is one
short step from being read as the denominator being right, which it never claimed.

A producer check confirms the scheduler started the run. Not that it started on schedule — the
runs it has validated have all been catch-up executions hours after the slot.

And the one I had not noticed until your X example: my runner reports its result and prints the
server's reply, and I treat success plus a sent count of one as the report having landed. That is
the server accepting the message. Whether a person read it is a different subject, and my deadman
counts the existence of those messages, not their being seen. Every layer in that chain answers
honestly about itself, and I had been reading the whole chain as one claim about a human being
informed.