Skip to content

Concepts

qnop has a small vocabulary, and almost everything it does follows from how these few objects relate. This page is the map; the rest of the documentation assumes it.

A document is uploaded and rendered. Reviewers mark passages, and each mark becomes an annotation carrying its own discussion thread. Every annotation ends in an explicit decision — accepted or rejected. Content changes create a new version, and existing annotations are carried onto it rather than orphaned. The review can only be finalized once no annotation is left open.

That last sentence is the product. Everything else exists to make it true.

Uploading a document creates the first version. Every later content change creates a new, immutable version — old versions are never rewritten and stay fully readable. A document is therefore a chain of versions, not a mutable file.

Binaries never live in the database. The uploaded file goes to S3-compatible object storage, and the database holds the relational data that points at it.

qnop reviews PDF today. DOCX and Markdown are decided and pending on the same ingest pipeline, but do not ship yet.

An annotation is not attached to a page number. It stores three layers at once:

LayerWhat it stores
Text quotethe exact wording, plus the text surrounding it
Positioncharacter offsets within the extracted text
Layoutthe region on the rendered page

Three layers rather than one is what lets an annotation survive a new version: when the content shifts, qnop re-anchors existing annotations onto the new version by fuzzy matching instead of dropping them. An inter-version diff shows what actually moved.

This is the mechanism behind the claim that a comment “keeps pointing at what it means” — a coordinate alone would not survive an edit.

Each annotation carries a Markdown thread: replies, reactions, and the context needed to decide. It also carries a status, and that status means something — it is a state, not a label someone remembers to set.

An annotation is OPEN until it is closed one of two ways:

StatusMeaning
OPENraised, still outstanding
RESOLVEDthe point was addressed — only the author may resolve it
DISMISSEDthe point will not be actioned — the review owner or an admin may

Both closed states can be reopened. Every one of these transitions is recorded in the audit trail. Silence never counts as agreement.

The review itself is a separate, explicit state machine — not a convention people are asked to follow:

The review workflow state machine A review starts in DRAFT and moves to IN_REVIEW. IN_REVIEW and CHANGES_REQUESTED alternate automatically: opening an annotation moves the review to CHANGES_REQUESTED, closing the last one moves it back. Once no annotation is open, the review can reach the terminal state FINALIZED, which earns the reviewer the "Closer" achievement. CANCELLED is a second terminal state, reachable from any non-terminal state, and earns nothing. 0 open an annotation opens the last one closes from any non-terminal state DRAFT IN_REVIEW CHANGES_REQUESTED FINALIZED CANCELLED terminal unlocks “Closer” terminal no achievement

The middle pair is the part worth understanding: IN_REVIEW and CHANGES_REQUESTED are derived from the open-annotation count, not set by hand. Raise an annotation and the review moves to CHANGES_REQUESTED on its own; close the last one and it moves back. Nobody has to remember to update a status field.

FINALIZED is gated. The transition is refused while any of three things is true: an annotation is still open, a placement is still pending confirmation, or no document version has finished extraction. That is what turns a review status into something you can plan around — when a review says it is finished, nothing quietly fell off the list.

Instances that need an escape hatch can set review.finalize_with_open_annotations, which resolves the remaining annotations as part of finalizing instead of refusing. It is off by default, and turning it on gives up exactly the guarantee above.

Access is modelled globally and per team:

  • ADMIN — administers the instance.
  • MEMBER — the ordinary reviewing role.
  • AUDITOR — a dedicated read role that exists to inspect the audit trail.

Teams sit on top of that, each with its own leads. A review can additionally run with anonymized reviewer identities when the process demands it — the contributions remain, the names do not.

One container serves the REST API and the embedded web UI. It needs PostgreSQL for relational data and any S3-compatible object storage for the documents themselves. Long-running work — text extraction, re-anchoring — runs as durable background jobs on PostgreSQL, so a crash never loses work.

See Deployment for what that means in practice.

qnop is open core. The main repository is the AGPL-3.0 community edition and is complete on its own: no user limits, no document limits, no feature gates.

Commercial add-ons are built in a separate repository against the published qnop-spi contract and load at runtime — the community edition is never degraded into a demo to make room for them. See Extending qnop.