nlqdb

Blog ·

An "open question" that's already decided is worse than one that's still open.

Decision logs rot at the seam between open and answered: a decided-but-unmarked bullet makes readers re-litigate closed calls. Make resolved a greppable state and count unmarked bullets as debt.

Every long-lived codebase grows a decision log — ADRs, a DECISIONS.md, per-feature records — and every one of those grows an Open questions section. The failure mode isn't the open questions. It's the entries that were quietly answered — in a PR, a standup, a founder's one-line reply — and never re-labelled. A bullet that reads "we should probably cap the queue at 7,000 ops/day" is a decision wearing an open question's clothes.

What a decided-but-unmarked bullet does to a reader

A reader who hits that bullet does one of two bad things. Either they treat the settled call as unsettled and re-litigate it — burning a design discussion on a question someone already closed — or they build on top of a "maybe" that was actually a "yes," and their design inherits a hedge that no longer exists. Both cost more than an honestly open question would, because the log looks authoritative while pointing nowhere. A vague decision documented is worse than none.

Two moves fix it

First, make resolved a first-class, greppable state. A decided bullet keeps its line but gains a marker word — Resolved, Decided:, Parked, or a strikethrough — plus a pointer to the decision's canonical home. The questions list is where a question's lifecycle is visible; it is never where the decision's body lives. When you answer an open question, the same commit moves the body to the canonical record and leaves only the marked pointer behind — never a second copy that can drift.

Second, count the unmarked ones as debt. If resolved is greppable, so is unresolved — and a number you can compute is a number you can drive to zero:

# Ambiguity debt: open-question bullets with no resolution marker.
# -h suppresses the filename prefix grep -A puts on context lines —
# without it the bullet match never fires and the count is always 0.
# Case-INSENSITIVE on the marker set — a case-sensitive grep counts
# "resolved" and "Resolved" differently and the number drifts.
grep -hA9999 '^## Open questions' docs/features/*/FEATURE.md \
  | grep '^\s*- ' \
  | grep -vciE 'resolved|decided:|parked|deferred|~~'

The case-insensitivity is not a nitpick — it's the difference between a metric and a mood. We track this count as a scorecard row, and the first version drifted for exactly that reason: two agents marked resolutions with different capitalization, the grep saw only one, and the "debt" number moved without any question changing state. Pin the counting method next to the number it produces.

Why a count, and not a cleanup day

A one-off sweep fixes today's rot and leaves the seam that produced it. The count makes the seam visible continuously: every answered-but-unmarked bullet is +1 debt that someone will notice, and driving one bullet to a marked resolution is a small, complete, verifiable unit of work — research the answer, write it in the canonical home, mark the pointer. A question only a human with prod access or a checkbook can answer moves to a separate blocked-on-human list and off the count, so the number only tracks debt an engineer can actually retire.

(This is a build note from nlqdb, the database you query in plain English. Our decision log is agent-operated, which makes the rot mechanical instead of cultural — but the fix is the same one a human team needs. Honest split: a decision-hygiene lesson, not a product feature.)

Try nlqdb in 30 seconds

No sign-in. The anonymous database lasts 72 hours; adopt it with one click if you keep it.

Start with a goal →

More posts: browse the blog.