Skip to main content

DEBT.UNRECORDED — a debt the database carries and the account has never heard of

  • Category: safety
  • Level: 0
  • Severity: info
  • Stability: stable
  • Suites: audit, deploy

The catalog shows a debt — a constraint added NOT VALID and never validated — and the committed debt account does not mention it.

That is a definite statement about the file rather than a gap in the reading. The database was queried, the object is there, and the account is silent about it.

Why this could not happen until now, and why that was the problem

Debts used to be recorded by one thing: a static rule that reads migrations. It works, and it has a blind spot that is exactly the shape of the debts worth the most.

A NOT VALID constraint that was already in the database when this package arrived has no migration to read. Nothing that reads migrations will ever find it. So it was reported on every single run, identically, with:

  • no first_seen, so no age — and age is the entire reason the account exists,
  • no way to write down a reason for carrying it,
  • no way to acknowledge it, so no way to stop it shouting.

A finding a team cannot act on is one they learn to scroll past, which is the outcome the debt account was built to prevent. The oldest debt a project carries was the only one with no date on it.

Recording it

php artisan sqlens:audit --debt=record

The default is --debt=check: compare, report, write nothing. An ordinary audit run still edits nothing at all.

What the date means, and what it does not

first_seen on an entry recorded this way is the day this project first looked. It is not the day the constraint was created — nothing available to this package knows that, and the statement that created it ran in some earlier deploy, possibly before the package existed.

That is honest and it is useful: an age measured from the day you started counting is still an age, and it still separates a debt somebody took on last week from one nobody has touched in a year. But it is a different sentence from the one on the entry beside it, so the entry says which it is:

{
"kind": "not_valid_constraint",
"object": "public.orders_user_id_foreign",
"migration": "",
"origin": "catalog",
"first_seen": "2026-08-23"
}

origin: catalog and an empty migration are the same statement written twice, on purpose: a reader sees it, and so does every consumer that reads the file — the reporters, the escalation path and the get_debt_ledger tool can branch on a field and cannot read this page.

One constraint is one entry, from either direction

A constraint that both sides can see does not produce two rows. The identity is derived from the debt's kind and its object and deliberately not from the migration, because the migration is how we came to know about it rather than what it is. Before that was true the two sides could not agree by construction, each reported an age computed from half the picture, and reconciliation could not converge — each side wanted to remove the other's row.

What a recording run will not do

It will not remove a debt it could not have seen. An audit run reads the catalog and never the migrations, so every migration-origin entry is outside what it looked at, and it is carried through untouched rather than treated as settled. "I did not look" and "it is not there any more" are different answers, and only one of them is safe to write to a file.