Premise Monitors — Living Claims¶
Every knowledge base stores claims whose truth quietly depends on facts elsewhere: a runbook names a script, a decision record cites a config file, an onboarding doc points at a directory. When the fact moves or disappears, the claim stays behind — confident, grounded, and wrong.
DocBrain attaches the checkable fact to the claim at capture time and keeps checking it. A premise is a machine-verifiable condition a knowledge fragment asserts — in v1, a file path in one of your connected git sources. A standing monitor re-verifies every premise on a cycle, and a claim whose premise died tells you so, with evidence.
The whole pipeline is deterministic: no LLM call, no extra network request in any check. A premise check is a set lookup against the file listing your git ingest already records. It behaves identically on a laptop and in production.
In plain terms¶
Every guide and runbook quietly depends on real things — "run this script", "edit that file". When those things move or get deleted, the document doesn't know. It keeps giving the old answer, confidently.
DocBrain writes down, at the moment knowledge is saved, exactly which real things it depends on. Then a watchman checks that list against your actual repositories, every five minutes, forever. The moment a fact your knowledge relies on disappears, that specific piece of knowledge is flagged — with proof: what vanished, when, and where it went if that can be determined. Your team gets a message; nobody has to reread anything.
And it never cries wolf: it only watches facts it has personally confirmed were true, it never guesses between possibilities, and when it can't check something it says "can't check" — never "broken".
Where premises come from¶
- Automatic extraction. When a fragment is indexed, path-shaped tokens in inline code
spans (a filename with an extension, e.g.
`scripts/rotate.sh`) are extracted as premises. Prose mentions, globs, URLs, flags, and placeholders are ignored — the same grammar the answer-time claim verifier uses. - Explicit declaration. Agents capturing over MCP can pass a
premisesarray todocbrain_annotateordocbrain_commit_capture— see Agent Capture. v1 checkspremise_type: "path"; other types are recorded but never checked (and never alert). Malformed entries are skipped item-by-item — a bad premise never fails the capture. The array is capped at 100 items; a capture declaring more is rejected with400rather than truncated, because the sweep reads every premise row in one unpaged query and that cap is what keeps its working set bounded by fragment volume. - A commit capture's changed files.
docbrain_commit_capturerecords why a change was made; the server turns each path in itsfile_pathslist into apathpremise, so the capture is checked against the files it describes and is flagged when one of them moves or disappears. Without it a commit capture could never be checked against the code again. Repeats collapse to one premise, and the same 100-item cap applies. A commit capture sent with no files declares nothing; it is served at once, labelled Not checked against code. - Backfill.
POST /api/v1/premises/backfill(admin) extracts premises for every already-indexed fragment that has none. Safe to re-run; it never duplicates.
The four states¶
| State | Meaning | Alerts? |
|---|---|---|
holds |
Verified true against a connected source's listing | — |
broken |
Was verified true, now decisively absent | yes — premise.broken |
uncheckable |
No connected source can currently speak to it | never |
dormant |
Never verified true | never |
Two rules keep this honest:
- Only a claim proven true can die. A premise that fails verification at capture is
born
dormantand stays silent forever — so a placeholder path in an example can never page anyone. If a later check finds a dormant premise true, it is promoted toholds(silently) and monitored from then on. - "I can't check" is not "it's gone." If a premise's source is disconnected or its
listing pruned, the premise becomes
uncheckable— an infrastructure fact, not an alarm. It keeps its last verdict and basis, so you can see what was known and when. When the source returns, the next sweep re-verdicts it; only a genuine death alerts.
The monitor¶
With PREMISE_MONITOR_ENABLED=true (the default), the server runs a standing monitor:
- Every 300 seconds by default (
PREMISE_SWEEP_INTERVAL_SECS) it sweeps all premises against the current file listings. - On fragment-index events it extracts and initially verifies a new fragment's premises immediately, rather than waiting for the sweep.
- At startup, and after any missed events, it backfills premises for indexed fragments that have none, so nothing is silently unmonitored.
- A discarded capture's premises are not swept. Discarding a capture rejects it, so its
claims stop being checked and stop being reported: they do not change state again, and
POST /api/v1/premises/checknever names a discarded capture. The rows are kept, frozen with the state and verdict they held at the moment of the discard — the same choice the discard itself makes, where the search entry goes and the capture stays with its reason — so the record of what was claimed survives the rejection. A capture that is merely waiting for review is not affected; only a rejected one stops being checked.
Writes happen only when something changed; a sweep over an unchanged world writes nothing and emits nothing. If the monitor cannot check (database hiccup, missing listing), premises hold their last-known state and age visibly — the system can be late and says so; it cannot be confidently wrong because checking was broken.
GET /api/v1/premises/health reports the last sweep time and counts. The Premises page in
the web UI shows a visible warning when the monitor has not swept recently.
Verdicts and evidence¶
Every decisive check records a verdict and a dated basis:
verified— the path exists (recorded with its resolved location).moved— the path is gone, but same-named files exist in the owning source. With exactly one candidate the verdict names it; with several it reports the count and refuses to pick — a confident guess is the failure this feature exists to remove.missing— gone, and no same-named file exists. Deleted, not moved.
The basis line — github:owner/repo at <commit> (captured <date>) — states what the check
was evidence of. A listing is a snapshot, never a timeless truth; an absence is always
dated to the commit it was observed at.
Surfaces¶
- Web: the Premises page — broken premises newest-first with verdicts and bases, a collapsed Uncheckable section, and monitor health.
- API:
GET /api/v1/premises/broken(any state via?state=, paginated, analyst+),GET /api/v1/premises/health(analyst+),POST /api/v1/premises/backfill(admin),POST /api/v1/premises/check(viewer; the pull-request check below). - Pull requests:
docbrain check-claims— see The pull-request check. - What is counted: a claim made by a document that has since been deleted is not current
content. The page,
/premises/healthand the pull-request check leave it out. The sweep still reads such rows, so beside its figure the page says how many of the rows it reads belong to deleted documents. - Events:
premise.brokenandpremise.restoredare emitted on state transitions, persisted to the event log, and deliverable by webhooks — subscribe an endpoint to those event types and a premise death is POSTed to it (signed) the moment the sweep detects it. Restoration (broken → holds) emitspremise.restored. - Answers: the monitor watches stored claims; the answer-time claim verifier
(
RAG_CLAIM_VERIFICATION) independently checks any path an answer cites against the same listings. An answer that cites a dead path carries its own correction note, whether or not the underlying fragment's premise has been swept yet. An answer's path, never seen to exist, is reported only when a connected repository holds its first two folders, so a one-folder path such asscripts/x.shis not flagged in answers; a premise that has held keeps the first-segment rule below, so it is still caught here.
The pull-request check¶
docbrain check-claims --diff <file|-> asks which live claims a change would falsify, for
the repository the change is in. A repository is named by its forge:
github:owner/repo or gitlab:group/project, GitLab nested groups kept whole
(gitlab:group/sub/project), lower-case, host dropped. The CLI makes the name on the
runner and sends only the name — never a URL, which in CI can carry a job token:
--repo <name or clone URL>;- GitHub Actions:
GITHUB_REPOSITORY(not on Gitea or Forgejo runners, which setGITEA_ACTIONS/FORGEJO_ACTIONS); - GitLab CI:
CI_MERGE_REQUEST_PROJECT_PATH(the merge request's target, also in a fork's pipeline), elseCI_PROJECT_PATH; - the
originremote (git remote get-url origin); an ssh alias is resolved once withssh -G; - none of these: exit 3, nothing sent.
A web URL is not a name: give the clone URL or the name. A local checkout is tied to a
repository by its own origin remote, read at ingest (on github.com, gitlab.com, or the
GitHub or GitLab host the deployment is configured for).
For each live claim on a removed path the server decides from recorded facts only: which repositories' listings hold the path, and the claiming document's own repository. A claim about the named repository is a finding; a claim about another repository is counted; anything that cannot be told is listed with its reason and one action — the page's own repository does not have the path, the page lives in a checkout DocBrain cannot name, a capture carries no repository while more than one repository holds the path, or DocBrain's listing does not include the path. A key restricted to spaces sees only those spaces, and the answer says so.
| Exit | Meaning |
|---|---|
0 |
Nothing at risk in that repository, or the change removes and renames nothing. |
2 |
Findings: the change falsifies live claims about this repository. --warn-only reports them and exits 0 — unless something could not be told. |
3 |
Could not run, or could not tell: no repository name; an empty diff on stdin; and, when the change removes or renames a path, a repository DocBrain does not read, claims it cannot tie to a repository, a key that may see no spaces, or a server too old to scope the check. Never 0, --warn-only included. |
The diff must be complete, or the check is answering a question nobody asked. A shallow
checkout has no base branch to diff against, git diff then fails, and in a pipe without
pipefail the check would read nothing. So fetch the history, write the diff to a file
under a shell that stops on error, and pass the file. (--diff - with an empty stdin is
refused with exit 3 for the same reason; an empty file means "nothing removed".)
# GitHub Actions
- uses: actions/checkout@v4
with:
fetch-depth: 0 # the base branch must exist locally
- shell: bash # bash -eo pipefail: a failed git diff fails the step
run: |
git diff --find-renames --name-status "origin/${{ github.base_ref }}..." > change.diff
docbrain check-claims --diff change.diff
# GitLab CI
check-claims:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
variables:
GIT_DEPTH: 0 # the default shallow clone may not hold the target branch
script:
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
- git diff --find-renames --name-status "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME..." > change.diff
- docbrain check-claims --diff change.diff
Limits, stated:
- Ingest reads a checkout's remote with
git -c safe.directory=<dir>, because in a container the mounted checkout belongs to another user. Git reads that setting from the command line from version 2.38; the DocBrain image ships a newer git. On an older git outside the image, each such checkout is logged once as "dubious ownership" and stays unnamed (its claims are "could not tell", never findings). - A key restricted to spaces sees only its spaces' claims, but the reasons can still say that another repository holds a path (listings carry no space); no claim, count or name from another space is shown. Whether a repository name is one DocBrain reads is likewise answered for any name the caller supplies.
- The premises page's sweep figures (checked, unsettleable, and how many of the checked rows belong to deleted documents) describe the whole corpus, not only the spaces a restricted key may see.
Behaviors worth knowing¶
These follow from the attribution rules — a premise belongs to sources whose listing contains its first path segment; a path in an answer, or one a capture declares when it is written, belongs to sources that hold its first two folders — and are deliberate trade-offs in favor of precision:
- Deleting a directory's only file reads as
uncheckable, notbroken. Ifproof/notes.txtwas the only file underproof/, removing it removes the whole top-level segment from the listing, so no source owns the claim any more. DocBrain says "nothing can speak to this" rather than guessing that a wider deletion was a targeted one. - A premise can re-attribute across sources. If two connected repos both have a
docs/tree, adocs/...premise is checked against both, and isbrokenonly when an owning source decisively lacks it. The answer-time verifier also checks against every owning source, but counts as owners only the sources that hold the path's first two folders. - Answers and new captures need two folders, premises need one. A path nobody has seen
exist (one an answer cites, or one a capture declares when it is written) is reported missing
only when a connected repository holds its first two folders, because top-level names like
docs/andscripts/are shared by most repositories and would otherwise flag files of codebases you never connected. A premise that has held was seen to exist, which is better evidence of what it is about, so it keeps the first-segment rule and still breaks when its one-folder file is deleted. - Precision over coverage, everywhere. One false "your runbook is wrong" costs more than ten missed ones. That asymmetry drives every rule above: deterministic oracles only, dated absences, no guessing between candidates, and silence whenever the evidence is incomplete.
What the monitor costs to run¶
Almost nothing, and document count is the wrong variable to worry about:
- Documents are not monitored — captured claims are. A thousand ingested documents create zero premises; only knowledge fragments carry them, and only their path-shaped claims. Large corpora typically produce hundreds of premises, not thousands.
- Each check is an in-memory set lookup — microseconds. A sweep is one small database read, one lookup per premise, and writes only for rows whose state or verdict actually changed. A sweep over an unchanged world writes nothing and emits nothing. A thousand premises is roughly a millisecond of comparison every five minutes; even a hundred thousand stays well under a second.
- The file inventory lives in memory and refreshes only when your repositories sync. The Kubernetes repository's ~31,000 paths measure a few megabytes. The monitor never clones anything, never reads document bodies, and never calls a model.
The heavy work — cloning repositories, embedding documents — happens at ingest time, where it always did. The monitor adds a five-minute heartbeat of list-against-list comparison.
Configuration¶
| Variable | Default | Description |
|---|---|---|
PREMISE_MONITOR_ENABLED |
true |
The standing monitor. Turning it off stops sweeps and events; premise rows and the answer-time verifier are unaffected. Helm: premises.monitorEnabled. |
PREMISE_SWEEP_INTERVAL_SECS |
300 |
Seconds between sweeps. The only dial on sweep cost — each sweep reads and settles every premise row. Values below 5 are raised to 5. Helm: premises.sweepIntervalSeconds. |
Premises need a git source with a recorded file listing — see the GitHub source in
Configuration. A deployment with no listings runs the monitor inertly:
every premise is uncheckable or dormant, and nothing alerts.