# Your own policies

How DPOLens reads your organisation's policies into clauses with keys, how a draft becomes a policy in force, and what happens to a citation when a policy changes.

DPOLens turns an organisation's policy, a Word or Markdown file, into clauses. Each one has
a key that citations point at, its text word for word, and its place in the document. No AI
model builds or changes that tree: it comes from the file itself, read with the techniques
legal publishers used long before software could guess.

You can see the result for any file on your computer. Nothing is stored:

```bash
dpolens policy parse retention-policy.docx --slug acme-retention
```

Uploading the file makes it a draft, which somebody reviews and publishes, and from then on
AI assistants search it and cite it. [From a draft to a policy in force](#from-a-draft-to-a-policy-in-force)
describes those steps, and [Keys over time](#keys-over-time) what happens to a citation when
a new version renumbers the clauses it points at.

## What it reads

Word files (`.docx`) and Markdown files (`.md`). Older Word files (`.doc`), PDFs and
OpenDocument files are refused with what to do instead: save the file as `.docx`, or use the
Word or Markdown original a PDF was made from. A file with no text, such as a scanned
document, is refused rather than read as an empty policy.

A Word file is read from its own XML, so text in tables, content controls, text boxes,
fields and tracked changes is all found. Tracked changes are read as if every change were
accepted, and the parse says so. Headers, footers, footnotes and comments are not part of any
clause; the parse says when a file has footnotes or comments.

A file is treated as untrusted. One that unpacks to more than 256 MB is refused before it is
unpacked, and so is any part that declares a DTD, which Word never does.

## How the tree is built

Each paragraph is read for three kinds of evidence, in this order:

1. **The file's own structure**: heading styles and outline levels, Word's automatic
   numbering, Markdown headings and lists.
2. **Its type**: a short line in bold, in larger type or in capitals, where the file gives
   it no heading style.
3. **Numbers written into the text**: `1.`, `4.2`, `(a)`, `(iv)`, `2-2-1`, `Article 5`, and
   the forms Arabic drafting uses, such as `المادة (5)`, `أولاً`, `١-` and `أ-`.

The first kind of evidence that says something about a paragraph decides where its clause
begins and how deep it sits. The others confirm it. Where they disagree, the paragraph is
flagged for a reviewer instead of guessed. Depth follows one rule: a new style of heading or
number goes one level deeper, and a style seen before goes back up to where it was.

What becomes a clause:

- **A heading**, with everything under it as its children.
- **A numbered paragraph**, which can hold deeper ones, as 3.5 holds 3.5.1.
- **Any other paragraph** of text.
- **Each row of a table.** A header row names the columns, and a row merged across the table
  titles the rows below it.

A bulleted list is not a clause of its own. It stays in the text of the line that leads into
it, so "We collect:" and the list after it are cited together.

## Keys

A key starts with the document's slug and adds one part for each level, each taken from the
document:

- **The clause's number**, as the document writes it: `acme-retention:3.5.1`, or
  `acme-privacy:4:1` for a list that starts again at 1 inside section 4.
- **Its heading's first words**, where it has no number:
  `acme-privacy:personal-data-we-collect:from-you`. Arabic headings keep their Arabic
  letters.
- **Its position**, for a paragraph with neither: `acme-privacy:security:p2`.

## Set aside

Some text in a file is not a clause anyone should be answered from: the document's title, a
cover page and the metadata, approval and version tables before the policy begins, a
contents page, text boxes, and a table printed again on later pages. That text is set aside
with its reason. It is kept and counted, so nothing goes missing quietly, and it is never
searched.

Before the policy begins, a line stays a clause only when it reads as a sentence: plain text
that ends with a full stop, a question or exclamation mark, or a colon. A line such as
"Owner: Information Security · Version 1.0 · Approved 10 October 2026" is set aside as front
matter, and a "Contents:" line that lists the sections' numbers is set aside as contents.

## Checks and flags

Every parse checks two things:

- **Coverage**: every character of the file is in exactly one clause or one set-aside block.
- **Numbering**: numbered clauses under the same parent run in order, with no gap and no
  repeat.

What the checks find, and every place where the evidence disagreed, becomes a flag: one thing
for a reviewer to settle. A heading with no number among numbered ones is flagged, because it
may belong under the heading before it. Two clauses that are both numbered 1-1 are flagged
rather than one of them renumbered.

## An example

```text
$ dpolens policy parse council-privacy.docx --slug council-privacy
Data Protection and Privacy Policy
Word file, 15 clauses. Of its 442 characters, 380 are in clauses and 62 set aside.

1  OVERVIEW                                                          file
  1:p1  The Council is open about how it collects and uses personal… file
2  PRINCIPLES                                                        file
  2:p1  The Council follows seven principles:                        file
  2:1  Lawfulness, fairness and transparency.                        file
  2:2  Purpose limitation.                                           file
  2:3  Data minimisation.                                            file
3  DATA SECURITY                                                     file
  3:p1  The Council keeps personal data secure.                      file
paper-storage  Paper Storage:                                        file
  paper-storage:p1  • Paper records are kept in a locked cabinet.    file
4  RETENTION SCHEDULE                                                file
  4:document-minimum-retention-period-reason  DOCUMENT · MINIMUM …   file
    4:document-minimum-retention-period-reason:staff-records  Staff… file
    4:document-minimum-retention-period-reason:minutes  Minutes      file

Set aside, not searched: title (1 paragraph), contents (3 paragraphs).

For a reviewer to settle (1):
  boundary   paper-storage  "Paper Storage:" has no number, unlike the headings around it,
  so it may belong under the one before it.
```

The last column says what decided each clause: `file` for the file's own structure, `type`
for its typography, `number` for a number written into its text. `--json` prints everything,
including each clause's full text and its language.

## From a draft to a policy in force

Uploading a policy reads it at once, so a file that cannot be read is refused there and then,
with what to do about it. A file it can read becomes a **draft**: the clauses, the flags, how
much of the file reached a clause, and what was set aside. Nothing searches a draft.

In the dashboard, **Policies** takes each step: upload a file, review the draft with the file as
it was read beside its clauses, settle each flag, and publish. The command line takes the same
steps:

```bash
dpolens policy upload retention-policy.docx --slug acme-retention --effective-date 2026-01-01
dpolens policy draft acme-retention
dpolens policy accept acme-retention 1 2
dpolens policy publish acme-retention
```

A reviewer checks each flag against the file and settles it: fixes how the clause was read
([Fixing what was read](#fixing-what-was-read)), accepts it as read, or, for a new version, says
a match is wrong. Publishing waits until every flag is settled. Uploading and
reviewing need the permission `documents.upload`; publishing and archiving need
`documents.publish`, which the seeded Admin and DPO roles hold. A draft's title, effective
date and version label can be changed until it is published (`dpolens policy edit`), and a
draft can be discarded (`dpolens policy discard`).

Two people can review one draft at once. A change made after somebody else's is refused, with
who made the other change and when, so nobody's review is undone without them knowing; the
draft reloads and the change can be made again.

Once published, a version never changes. Searches find it from the day it takes effect: by its
words straight away, and by meaning once the worker has embedded its clauses. The API reports
how far that has got as `indexing`.

Versions take effect in the order they are published: each one after the one before it. Each
has a label that citations show, `1`, `2` and so on unless you give it another, and no two
versions of a policy share one, because a citation names its version by its label.

The [HTTP API](https://dpolens.com/docs/api#policies) takes the same steps, with the file as the request body:

```bash
curl --data-binary @retention-policy.docx \
  -H "Authorization: Bearer $DPOLENS_TOKEN" -H "Content-Type: application/octet-stream" \
  "http://localhost:8000/v1/policies?filename=retention-policy.docx&slug=acme-retention"
```

On an instance started with Docker Compose, the command line runs in the `api` container, so
copy the file in first:

```bash
docker compose cp retention-policy.docx api:/tmp/
docker compose exec api dpolens policy upload /tmp/retention-policy.docx --slug acme-retention
```

Every step, from the upload to the publication, is written to the governance log with who took
it, and the upload's entry records the file's SHA-256.

## Fixing what was read

When the parser reads a clause wrongly, a reviewer fixes it in the draft, and no word changes.
There are four tools:

- **Split** a clause where a second rule begins: where a sentence or a paragraph does, or where
  a number follows a semicolon or a colon. A number where the second part begins becomes its
  key. A clause with clauses under it is split once they are moved out.
- **Merge** a clause with the one before it, when a page break or a stray line cut one rule in
  two. Text merges with text, a heading with a heading, a row with a row, and a merged clause's
  number stays in its words.
- **Make a heading text**, or **make text a heading**. What was under a heading made text
  follows it at its level. A heading is one paragraph, so a longer clause is split first.
- **Move a clause under the one before it**, or **out a level**. Moving out brings the clauses
  after it at its old level along, in the file's order: a heading holds them, and any other
  clause is followed by them.

Keys follow from where each clause ends up, by the rules a parse uses: moving `personal-use`
under section 3 makes it `acme-aup:3:personal-use`. Each clause a fix moved or reshaped says
`you` where the others say `file`, `type` or `number`.

A fix settles the flags it answers. An open flag on the clause fixed is settled by the fix, and
the checks run again: a numbering flag the fix makes untrue is settled too, and a problem the
fix makes, such as two clauses numbered 4.2, is a new flag. For a new version, a clause whose
words a fix changes is matched again against the clauses of the version before that nothing
continues, so a new file fixed the way the last one was matches it clause for clause.

Where the parser knows the fix for a flag, the flag suggests it: moving a heading with no number
under the heading before it, making a paragraph with a heading's style a heading, or splitting
a clause where a missing number is typed into it, as when "3.4 follows 3.2" and "3.3" is inside
3.2.

In the dashboard, the tools are on the clause selected, and a flag's suggested fix is a button
on the flag. On the command line:

```bash
dpolens policy fix acme-aup personal-use move-under
dpolens policy fix acme-aup 3.2 split --before "3.3 Clear"
dpolens policy fix acme-aup 4:p1 merge
```

The others are `make-text`, `make-heading` and `move-out`. Every fix is written to the
governance log.

## A new version

A new version of a policy is uploaded against the policy it replaces:

```bash
dpolens policy revise acme-retention retention-policy-v2.docx --effective-date 2026-07-01
```

Each clause of the new version is matched to the clause of the version before it that it
continues: first by identical words, then by the same number with at least half the same
wording, then by a different number with at least four fifths the same wording. Each clause is
then `same`, `reworded`, `new`, or renumbered (`was 4.2`), and a clause of the version before
that nothing continues is listed as removed.

Matching by wording can be wrong, because two rows of a retention schedule can share most of
their words, so four things become flags for a reviewer: a clause whose number changed, a
clause whose words changed so much that it may be a different rule (the flag quotes what it
said before), a number that named a different rule in the version before, and a new clause
that shares most of its words with a clause of the version before that nothing continues,
which may be that rule, renumbered and reworded more than matching pairs without asking. A
policy has one draft at a time. Uploading another while one is open is refused, unless you ask
for the new file to replace it (`--replace`).

When a match is wrong, say so, with **It's a different rule** in the dashboard or on the
command line:

```bash
dpolens policy reject-match acme-retention 4.3
```

The clause becomes new, and the clause of the version before that it was matched to becomes
removed. A citation of that earlier clause then says its rule was removed, instead of pointing
at a different rule that happened to share its words.

When two clauses are the same rule and matching left them apart, say that too. The flag on a
new clause that shares most of an earlier clause's words shows the two side by side and asks:
**It's the same rule as 4.3**, or **It's a different rule**. Each clause is asked about once,
the closest pair first. Any other new clause can be paired with **It's the same rule as…**,
which lists the clauses of the version before that nothing continues, closest first. On the
command line:

```bash
dpolens policy same-rule acme-retention 4.4 4.3
```

The new clause then continues the earlier one as if matching had paired them, so a citation
of the earlier clause says where its rule is now. Only a clause nothing continues can be
named, and each of these two decisions undoes the other. Both are written to the governance
log.

## Keys over time

A policy's keys follow the numbers printed in it, so a reader can always find a key in the
document in front of them. When a new version inserts a clause, the clauses after it are
printed with new numbers, and their keys change with them:

```text
Version 1, in force from 1 January         Version 2, in force from 1 July
4.1  Customer records: 2 years              4.1  Customer records: 2 years
4.2  Staff records: 6 years                 4.2  Contractor records: 1 year     new
4.3  Supplier records: 5 years              4.3  Staff records: 6 years         was 4.2
                                            4.4  Supplier records: 6 years      was 4.3
```

A citation names its version as well as its key, and that is what keeps it exact. A citation
of `acme-retention:4.2` in version 1, read as of a date when version 1 was in force, still
gives the staff rule word for word, and says where that rule is now:

```text
$ dpolens clause show acme-retention:4.2 --as-of 2026-03-01
...
4.2  [acme-retention:4.2]
Staff records are kept for 6 years after employment ends.

In version 2, in force since 2026-07-01, this rule is acme-retention:4.3, word for word.
```

Read today, `acme-retention:4.2` is the contractor rule, and says what it named before:

```text
In version 1, acme-retention:4.2 was a different rule, which is acme-retention:4.3 now:
  Staff records are kept for 6 years after employment ends.
```

The HTTP API and the MCP server say the same, as `now` and `formerly`, so an AI assistant reading
an old citation is told where the rule went.

## Versions in the library

A policy's page in the library lists its versions: the one in force, one published to take
effect later, and those it replaced, each with the day it took effect and, once another took
over, its last day in force. Any of them can be read as it stood, and each one's original file
downloads from the list. Whoever reviews policies also sees the open draft there, with how many
flags it has left.

Any two published versions can be compared, clause by clause: what was added, renumbered,
reworded and removed, with the words that changed marked. Each rule is followed from one version
to the next through the matches made when each was uploaded and reviewed, so a match a reviewer
rejected, or a clause they fixed, counts as it did then. On the command line:

```bash
dpolens policy versions acme-retention
dpolens policy compare acme-retention 1 3
```

A clause read in a version no longer in force says so in the dashboard, with the days that
version was in force and where the rule is today, and when its key names another rule today, the
page shows that one too.

## Archiving

Archiving a policy takes it out of search and out of the library. Nothing is deleted: its
clauses still read by key, marked as archived, so a citation of one still resolves. Publishing
a new version of an archived policy brings it back.

```bash
dpolens policy archive acme-retention
```

## The original file

The file each version was read from is kept, under a name made from its SHA-256, and checked
against that hash whenever it is read, so a file that went missing or changed is an error
rather than a different download. Anyone who may read the library can download a published
version's file; a draft's is for whoever uploads or publishes policies. An upload writes the
file before its draft, so a failure can leave a file that nothing points at, and the worker
removes those once a day, after a day's grace.

What the reader found in the file is kept with each version too: every paragraph in order, what
it looked like, and the clause it went to or why it was set aside. That is the file the review
shows beside the clauses, and it stays the same for the life of the version.
[configuration.md](https://dpolens.com/docs/configuration) says where the files are kept.

## The sample policy

A new instance starts with a made-up policy for a made-up company,
`sample-data-protection`, so there is a policy of your own kind to search before yours are in.
Archive it when they are. It is published once: archived, it is never brought back.
