Writing "I'll fix this" at the end of a post costs nothing. It feels good while you type it: the flaw has been named, owned, and put in the queue. The reader finishes that sentence, thinks "fine, he knows," and closes the tab.
Then nobody goes back to look. Not the reader, not me.
This morning I got curious: how many times have I made that kind of promise on this blog, and how many did I keep? The pleasant part of the question is that it's measurable — the promises sit in published text, and the outcomes sit in the same repository. The unpleasant part is that once I had the answer, I had to write it up.
I wrote the scanner, then distrusted the scanner
I split the body of 1,387 Turkish posts — excluding code blocks and diagrams — into sentences and looked for two families of pattern. The first is first-person future commitments: I'll fix, I'll build, I'll measure, I'll add, I'll move. The second is open-work declarations: I haven't set this up yet, open item, it's on my list. The script ships in this post's commit; run it with --before 2026-11-02 and you get exactly the numbers below, because that flag excludes this post from the corpus. One limit up front: the script only reads the Turkish files, so promises made in the English twins are entirely outside this count.
The script flagged 543 sentences. Of those, 399 were unclassifiable negative past tense — "nobody called me" and the like — 18 were method-limit notes ("didn't measure it, derived it"), and 9 were deliberate decisions. I set all three aside. That left two workable sets: 63 future commitments across 53 files, and 54 open-work declarations across 43 files. One hundred seventeen candidates.
I read all 117 one by one, and most of them turned out not to be promises at all.
| What the scanner found | Count | What it actually was |
|---|---|---|
| In-text signpost | 16 | "I'll come back to this" — closes in the same text |
| Advice to the reader / narrative | 23 | "The first place I'd look is…" — a tip, not a commitment |
| Generic sentence, version note, decision | 21 | "This kernel doesn't have it yet" — nothing to do with me |
| Technical idiom, observation note | 24 | "A transaction left open" — not a task, a connection |
| Real commitment | 33 | Something I told a reader I would do |
That fourth row is the scanner's prettiest mistake. In Turkish, the adjective açık covers both "open" as in unfinished work and "open" as in an open connection, so the script filed long-running database transactions, browser tabs left open, and PoC environments that had stood for weeks into my debt list. The script can't tell the two apart; I can. That is exactly why this scan is a reading list, not a gate.
Two of the 33 sentences were the same promise twice: a job I ended a 12 September post with ("I haven't turned it on my own server yet; it's on the list") got mentioned again in the next day's opening. After deduplication, 32 distinct commitments remained.
Not all 32 came from the patterns. While reading the matched posts I ran into promises the scanner had never flagged — the 19 October sentence "I need to put it alongside" among them, because koymam gerekiyor isn't in my verb list. So 117 candidates is a floor, not a ceiling. I don't know the ceiling, and saying so is this post's first debt of honesty.
So my false-positive rate is 72 percent. Eighty-four of the 117 candidates weren't promises. I'm writing that number down deliberately, because the day I counted 37 dead references my false-positive rate came out at 92 percent, and that day I gave myself a rule: never trust a scan's result without interrogating the scanner. This time I applied the rule. It is also the only rule I applied, as you'll see shortly.
The rule earned its keep in one more place, and this time the fault was in the scanner itself. In the first Node version the word boundary was a plain \b, and in JavaScript \b only knows ASCII. It treated the Turkish suffix following bakacağım ("I'll look") as outside the word and let in unrelated sentences like bakacağımı and uygulayacağımız. The same flaw ran the other way too: ölçeceğim ("I'll measure") begins with a Turkish letter — ö isn't a letter in ASCII either — so that verb was never seen at all.
To size the effect, the verb list stayed fixed and only the boundary changed. With the ASCII boundary, 80 sentences; with a Turkish letter class, 63. The intersection is 59 — so the ASCII version had pulled in 21 irrelevant sentences and missed 4 real promises. Twenty-seven percent of inflation, and the inflated number is the easiest one to put in a headline.
I could ask a record about 13 of the 32
This is where the job changed character. I couldn't audit all 32 — I'll come to why shortly, because that's the real finding of this post. The number of promises I could put to a file, a DNS record, an API response or the git history is 13.
Of those 13, 11 are still open. One isn't due yet, and one has a condition I can't observe. Kept: zero.
| Promise | Date | Age | Record today |
|---|---|---|---|
Add np/sp to the DMARC records | 29 Aug | 65 days | neither is in the live TXT |
| Turn the near-duplicate scan into a standing test | 5 Oct | 28 days | no script, no test, no allowlist |
Put required checks on main | 8 Oct | 25 days | branch unprotected |
| Fix the seven wrong lines in my recipe | 10 Oct | 23 days | at least one still wrong |
| Write the test comparing the recipe's number to the code | 10 Oct | 23 days | no test reads the document |
| Push a copy to the Wayback Machine on publish | 12 Oct | 21 days | appears in no script |
| Make the alarm "final stop ≠ cited address" | 12 Oct | 21 days | that comparison doesn't exist |
Add ANALYZE at the end of migrate | 14 Oct | 19 days | absent |
The Fixes: habit | 15 Oct | — | 112 commits, 0 lines (conditional) |
| Filter the English archive, publish the result | 17 Oct | 16 days | no post appeared |
| Put this scan's script in the repo | 19 Oct | 14 days | not under scripts/ |
| Move tag validation into the schema | 26 Oct | 7 days | schema is still free text |
| Re-run the recidivism script in three months | 15 Oct | — | due 15 January 2027 |
The row that surprised me most while building that table was the first one, because I thought I had kept it.
The promise I thought I'd kept
On 29 August I wrote about DMARC and cut a notch somewhere with this sentence: "Only one of my five records has sp, and none has np. That's the first thing I'll fix after this post."
Today I looked at the DMARC record for this blog's domain — I keep my data scope limited to my own infrastructure, so that's the only one I'm looking at. The record has p=reject, it has pct=100, it has the reporting addresses. No np. No sp. The job I called "the first thing I'll fix after this post" sixty-five days ago is sitting exactly where it was.
That record has aged in another direction too. RFC 7489, the source cited in that post, is now obsolete: RFC 9989 states in its own abstract that it obsoletes RFC 7489 (and 9091). RFC 9989 removed the pct tag, and the reason is in its own text: unless the value was 0 or 100, it was "usually not accurately applied." My pct=100 matches the old default, so it changes no behaviour; it just tells you which year the record is from. The p tag is no longer required either, only recommended.
Incidentally, the body of that 29 August post describes RFC 9989 correctly, because the source was opened and read while writing. So the post stayed current and my record didn't.
Code that is older than the promise
The most instructive finding comes from 12 October. That day I had measured what fraction of my cited addresses had moved, and I ended with this: "The alarm criterion isn't 404. The real signal is 'final stop ≠ the address I cited'."
To check it, I looked at the source gate and at first glance thought the promise had been kept, because validate-content-sources.mjs contained precisely the name I was looking for: finalUrl. It takes the post-redirect address, checks something, and decides.
Two things went sideways. First, that code doesn't compare the redirect target to the address I cited; it checks it against the allowlist. It asks "is the final stop on a trusted domain," not "is the final stop the address I wrote." A reasonable question — just not the one I promised.
Second, and more irritating: I asked git log -S how old the line was. 17 August. Fifty-six days older than my promise. So where I wrote "I'll do this" on 12 October, there already sat code written two months earlier for an entirely different reason, and I mistook it for my own work.
There's a scope problem too. That gate only runs against files being staged for publication. It never asks about the addresses in the 1,387 posts already out there. What I thought was an alarm is really a turnstile.
A subordinate clause in the docs voided the promise
On 8 October, after measuring the pull requests I had merged without waiting for the reviewer, I wrote: "So I'll change a setting instead of my intentions: protect main, and make the checks required rather than reviewer approval."
Today the branch-protection query returns 404 Branch not protected. I checked the newer mechanism too, since protection can now be configured two different ways: the repository has exactly one ruleset, it's called deploykey, it comes down from the enterprise level, and it has nothing to do with this. The promise is twenty-five days old with nothing behind it.
But that isn't the lesson. When I opened GitHub's documentation and read it, I saw that the promise wouldn't have done the job I expected even if I had kept it. The docs define required checks plainly: "all required status checks must pass before collaborators can merge changes into the protected branch." Right below that sits the line I never considered on 8 October: by default, a branch protection rule's restrictions don't apply to people with admin permissions on the repository. You have to enable that separately.
In a one-person repository, I'm the admin. So if I had ticked the box and written "now I make myself wait," I could have merged without waiting again the next day, and it would have taken a fresh measurement to notice. What was wrong with that promise wasn't my laziness; it was written incompletely. "Add protection" is an intention, not a promise — and without "close the bypass too," that intention does nothing even when it fulfils itself.
The number in my own recipe
On 10 October I tested seven claims in my production recipe against the code and found all of them wrong. I ended that post like this: "this post hasn't fixed a single one of the seven lines… I've written the fix into the next step."
Twenty-three days later I checked the line that is easiest to verify. The code that computes reading time is a single line: const WORDS_PER_MINUTE = 200;. What the recipe says: readingTime: <words/250, rounded>. Still two different numbers, still the same two files.
In that same post I wrote four rules, and the third one existed precisely to solve this: wire the gate list to automation, write a test that reads the number from the document and compares it to the code. Here I have to be careful, because the constant already has a guard: content-policy.test.mjs expects 3 minutes for a 401-word body, which only holds at 200 — move the constant to 250 and that test breaks. What won't break is the document. None of the sixteen files in the test suite opens the recipe and reads the number inside it, so the code is protected and the sentence describing it is not. My promise was exactly that second half.
The strange part is that I already knew this while writing. "This post doesn't apply the third rule either, I'm aware," I wrote. Being aware changed nothing in twenty-three days.
On 19 October I had written the right rule
After assembling that whole table, I found the sentence in my own archive that bothers me most. On 19 October I set four rules for my own documentation, and the third reads: "I will never write unfinished work as plain prose. A checkbox is free and lasts five months."
That rule was written for documents, not for posts. But it diagnosed the mechanism correctly: open work buried in prose disappears, open work written as - [ ] survives. After that date, posts published on this blog went on writing open work as plain prose. On 31 October I ended one with this sentence: "Is there a fix for this? I haven't built it yet." Exactly the form I had described, in exactly the place where it gets lost.
In that same post I grounded rule 2 in architecture decision records, and there I overreached. I opened the source today and read it. The page lists Nygard's ADR format as "title, status, context, decision, and consequences" — so the status field really is there. But I wrote "all of the templates"; the Y-Statement format on that same page has no status field. The phrase "records aren't deleted" appears nowhere on it either; the page says nothing about the lifecycle of a record. My rule stands, its citation was more confident than the source. I'm correcting that here.
Out of curiosity I went looking for something similar in the canonical text on postmortem culture — it surely says action items need an owner and a place to be tracked. It doesn't. The page lists five review criteria it says a review "might include," and the only one that touches action items is this: "Is the action plan appropriate and are resulting bug fixes at appropriate priority?" The appropriateness of the plan is asked about; whether it was recorded anywhere is not. I couldn't find the rule I was looking for in the literature — it wasn't where my own trained memory told me it was.
The 19 promises I didn't ask about
I could not put a record to 19 of the 32 promises, and in most of them the reason is embarrassingly simple when you look one by one: those promises have no object.
"From now on I'll look a little more sceptically at every flag I haven't measured." I wrote that on 10 October. Which file should I look at? "What I'll change is the question I ask while writing." That's from 22 October. Which query verifies it?
Both are honest sentences. Both describe a change in attitude, and a change in attitude is a real thing. But an attitude leaves no record; only its consequences do, and only if you can make the consequences countable. There are others like them: look at a dashboard again tomorrow, watch the metrics of a pipeline that no longer exists, change the permission line of a browser extension.
But I said "most," not "all" — and if I don't write down the difference, this post breaks its own rule. At least four of the nineteen have a perfectly good object; I simply didn't ask about them today. Yesterday's post about cover images enumerates four jobs one by one; the 28 October keepalive measurement says plainly that the fix was never applied on the live server; the Cloudflare setting I called "on the list" on 12 September is still on the list; and there are the two things I said on 7 October I would fix. All four are auditable. I stopped at thirteen. So the 11 in the table isn't the full set of my open promises — it's the ones I found.
Even so, 19 unsettled me more than 11. A known open promise is a debt list; you can close that in an afternoon. A promise with no object is a measurement gap: whether I keep it or not, I won't be able to tell, because the two states look identical. An empty result resembling a correct result has caught me in this repository before; that day I was counting TODOs in code comments, and today my own sentences.
Three things that make a promise auditable
The framework I take away from this measurement is short, because I have now measured what writing long lists accomplishes.
An object. If a promise names no file, record, setting or command, it cannot be audited. Instead of "I'll be more sceptical," write "I'll add a case under scripts/tests/ that reads this constant." A promise with an object defends itself six months later with a single grep.
A comparison. When you go looking for what fulfilled a promise, check whether what you found existed before the promise. My finalUrl line was fifty-six days older than my sentence, and I took it for my own work. One git log -S call ends that illusion in half a minute.
The condition. Read the documentation while designing the promise, not while writing it. "Turn on protection" was an empty promise from the start, because it skipped the subordinate clause that says the rule doesn't apply to admins by default. Even kept, it would have produced no measurement.
There is also one rule I'm applying in this post's commit: the scan's script goes into the repository. I promised the same thing on 19 October and didn't do it, so this time I wrote the script instead of the sentence.
Conclusion
The hard part of keeping an open-work list isn't writing the list. The hard part is making sure that work which never makes it onto the list still leaves a record somewhere.
In my case, 1,387 posts produced thirty-two unrecorded promises. I put thirteen of them to a record, and eleven came back open. For the remaining nineteen there isn't even a number yet, and for most of them there never will be — that second group is more dangerous than the first, because it never reaches the debt list at all.
Honesty and accountability are not the same thing. Writing "I haven't done this yet" at the end of a post is honesty; writing that sentence in a form somebody — at minimum you — can find six months later is accountability. The second costs no more than the first. It just requires writing a filename instead of a sentence.
This post carries a promise of its own, and I wrote its object on purpose: the eleven open rows in the table above, and re-running scripts/measure-article-promises.mjs without the --before flag on 2 February 2027. You'll be able to count both of them independently of me — which is, in fact, this post's only genuine novelty.
