- Docs
- Your own policies
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:
dpolens policy parse retention-policy.docx --slug acme-retentionUploading 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 describes those steps, and 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:
- The file's own structure: heading styles and outline levels, Word's automatic numbering, Markdown headings and lists.
- Its type: a short line in bold, in larger type or in capitals, where the file gives it no heading style.
- 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, oracme-privacy:4:1for 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#
$ 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:
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-retentionA reviewer checks each flag against the file and settles it: fixes how the clause 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 takes the same steps, with the file as the request body:
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:
docker compose cp retention-policy.docx api:/tmp/
docker compose exec api dpolens policy upload /tmp/retention-policy.docx --slug acme-retentionEvery 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:
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 mergeThe 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:
dpolens policy revise acme-retention retention-policy-v2.docx --effective-date 2026-07-01Each 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:
dpolens policy reject-match acme-retention 4.3The 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:
dpolens policy same-rule acme-retention 4.4 4.3The 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:
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.3A 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:
$ 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:
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:
dpolens policy versions acme-retention
dpolens policy compare acme-retention 1 3A 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.
dpolens policy archive acme-retentionThe 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 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.